Skip to content

Commit

Permalink
Refactor UI logger (more work needed)
Browse files Browse the repository at this point in the history
  • Loading branch information
danpashin committed Jun 19, 2024
1 parent 288f2f0 commit 0aa1aaa
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 84 deletions.
4 changes: 4 additions & 0 deletions twackup-gui/Twackup.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
0B48B1BA2C22B2CD00D21737 /* DpkgListVC+BlankSlate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B48B1B92C22B2CD00D21737 /* DpkgListVC+BlankSlate.swift */; };
0B48B1CF2C22E47A00D21737 /* libroot.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B48B1C12C22E43B00D21737 /* libroot.a */; };
0B48B1D62C22E6DD00D21737 /* dyn.c in Sources */ = {isa = PBXBuildFile; fileRef = 0B48B1D42C22E6D600D21737 /* dyn.c */; };
0B48B6A42C23600400D21737 /* StyledLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B48B6A32C23600400D21737 /* StyledLogger.swift */; };
0B48EF3B2C1EF8E6007C0EC0 /* DatabasePackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B48EF3A2C1EF8E6007C0EC0 /* DatabasePackage.swift */; };
0B48EF3D2C1F488A007C0EC0 /* Sequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B48EF3C2C1F488A007C0EC0 /* Sequence.swift */; };
0B48EF3F2C1F4F87007C0EC0 /* UnfairLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B48EF3E2C1F4F87007C0EC0 /* UnfairLock.swift */; };
Expand Down Expand Up @@ -105,6 +106,7 @@
0B48B1C12C22E43B00D21737 /* libroot.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libroot.a; sourceTree = BUILT_PRODUCTS_DIR; };
0B48B1D42C22E6D600D21737 /* dyn.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = dyn.c; path = src/dyn.c; sourceTree = "<group>"; };
0B48B1D52C22E6D600D21737 /* libroot.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = libroot.h; path = src/libroot.h; sourceTree = "<group>"; };
0B48B6A32C23600400D21737 /* StyledLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StyledLogger.swift; sourceTree = "<group>"; };
0B48EF3A2C1EF8E6007C0EC0 /* DatabasePackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabasePackage.swift; sourceTree = "<group>"; };
0B48EF3C2C1F488A007C0EC0 /* Sequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sequence.swift; sourceTree = "<group>"; };
0B48EF3E2C1F4F87007C0EC0 /* UnfairLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnfairLock.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -374,6 +376,7 @@
0B6BB2F0294A599D001E3B43 /* ProxiedObservedObject.swift */,
0B5B332A294DA9F00097398F /* MainModel.swift */,
0B86CDA72C18D8F000F3B4A0 /* ConsoleLogger.swift */,
0B48B6A32C23600400D21737 /* StyledLogger.swift */,
);
path = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -683,6 +686,7 @@
0B50FF702949B82900CF4BCF /* CapacityView.swift in Sources */,
0B48B1B82C22B04600D21737 /* DebsListVC+BlankSlate.swift in Sources */,
0B6BB2F1294A599D001E3B43 /* ProxiedObservedObject.swift in Sources */,
0B48B6A42C23600400D21737 /* StyledLogger.swift in Sources */,
0B2636E62948B92E003EA806 /* SettingsViewController.swift in Sources */,
0B88C3072930B2FB00BA5312 /* PackageSection.swift in Sources */,
0B434F982935DEFA00311FB6 /* RebuildPackageDetailedView.swift in Sources */,
Expand Down
143 changes: 143 additions & 0 deletions twackup-gui/Twackup/Sources/Models/StyledLogger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
//
// StyledLogger.swift
// Twackup
//
// Created by Daniil on 19.06.2024.
//

import StyledTextKit

actor StyledLogger: FFILoggerSubscriber {
struct RenderOptions {
let boldFont: UIFont

let monospacedFont: UIFont

let color: UIColor
}

struct RenderIssue: Error {
init() {
}
}

struct RenderResult {
let image: CGImage

let size: CGSize

let scale: CGFloat

let insets: UIEdgeInsets
}

private let uuid = UUID()

private let builder = StyledTextBuilder(text: "")

private var cachedRenderer: StyledTextRenderer?

private(set) var wantsRerender = false

let renderOptions: RenderOptions = {
let systemSize = UIFont.systemFontSize
let bold = UIFont.boldSystemFont(ofSize: systemSize)
let monospaced = UIFont.monospacedSystemFont(ofSize: systemSize, weight: .regular)
return RenderOptions(boldFont: bold, monospacedFont: monospaced, color: .label)
}()

init() {
Task {
await FFILogger.shared.addSubscriber(self)
}
}

deinit {
Task {
await FFILogger.shared.removeSubscriber(self)
}
}

func render(for width: CGFloat, contentSizeCategory: UIContentSizeCategory) throws -> RenderResult {
if wantsRerender {
wantsRerender = false
cachedRenderer = nil
}

if let cachedRenderer {
let cachedResult = cachedRenderer.cachedRender(for: width)
if let cachedImage = cachedResult.image, let cachedSize = cachedResult.size {
return RenderResult(
image: cachedImage,
size: cachedSize,
scale: cachedRenderer.scale,
insets: cachedRenderer.inset
)
}
}

let renderer = StyledTextRenderer(string: builder.build(), contentSizeCategory: contentSizeCategory)
cachedRenderer = renderer

let (image, size) = renderer.render(for: width)
guard let image else {
throw RenderIssue()
}

return RenderResult(
image: image,
size: size,
scale: renderer.scale,
insets: renderer.inset
)
}

func render(for width: CGFloat, contentSizeCategory: UIContentSizeCategory) async throws -> RenderResult {
try await withCheckedThrowingContinuation { continuation in
continuation.resume(with: Result {
try render(for: width, contentSizeCategory: contentSizeCategory)
})
}
}

// MARK: - FFILoggerSubscriber

func log(message: FFILogger.Message, level: FFILogger.Level) async {
let targetColor: UIColor = switch level {
case .off: .clear
case .debug: .systemIndigo
case .info: .systemBlue
case .warning: .systemOrange
case .error: .systemRed
}

builder.add(text: "[\(message.target ?? "nil")] ", attributes: [
.font: renderOptions.boldFont,
.foregroundColor: targetColor
])

builder.add(text: message.text, attributes: [
.font: renderOptions.monospacedFont,
.foregroundColor: renderOptions.color
])

builder.add(text: "\n")

wantsRerender = true
}

func flush() async {
builder.clearText()
wantsRerender = true
}

// MARK: - Hashable

static func == (lhs: StyledLogger, rhs: StyledLogger) -> Bool {
lhs.uuid == rhs.uuid
}

nonisolated func hash(into hasher: inout Hasher) {
hasher.combine(uuid)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ extension LogViewController: @preconcurrency BlankSlate.DataSource, @preconcurre
}

func blankSlateShouldDisplay(_ view: UIView) -> Bool {
MainActor.assumeIsolated { currentText.length == 0 }
// MainActor.assumeIsolated { currentText.length == 0 }
false
}
}
104 changes: 22 additions & 82 deletions twackup-gui/Twackup/Sources/ViewControllers/LogViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,18 @@
import BlankSlate
import StyledTextKit

final class LogViewController: UIViewController, FFILoggerSubscriber, ScrollableViewController {
final class LogViewController: UIViewController, ScrollableViewController {
let metadata: ViewControllerMetadata

let mainModel: MainModel
private let styledLogger = StyledLogger()

let currentText = NSMutableAttributedString()

private var wantsToScrollBottom: Bool = false

private(set) lazy var logView: StyledTextView = {
let view = StyledTextView()

return view
}()
private let textView = UIView()

private(set) lazy var scrollView: UIScrollView = {
let view = UIScrollView()
view.isScrollEnabled = true
view.alwaysBounceVertical = true
view.addSubview(logView)

view.bs.setDataSourceAndDelegate(self)
view.addSubview(textView)

return view
}()
Expand All @@ -39,21 +29,10 @@ final class LogViewController: UIViewController, FFILoggerSubscriber, Scrollable
return UIBarButtonItem(title: title, style: .plain, target: self, action: #selector(actionClearLog))
}()

init(mainModel: MainModel, metadata: ViewControllerMetadata) {
self.mainModel = mainModel
init(metadata: ViewControllerMetadata) {
self.metadata = metadata
super.init(nibName: nil, bundle: nil)
tabBarItem = metadata.tabbarItem

Task {
await FFILogger.shared.addSubscriber(self)
}
}

deinit {
Task {
await FFILogger.shared.removeSubscriber(self)
}
}

required init?(coder: NSCoder) {
Expand All @@ -74,74 +53,35 @@ final class LogViewController: UIViewController, FFILoggerSubscriber, Scrollable
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

renderLog()
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

scrollView.bs.reloadBlankSlate()
scrollToBottomIfNeeded()
}

private func renderLog() {
let category = UIApplication.shared.preferredContentSizeCategory
let builder = StyledTextBuilder(attributedText: currentText)
let renderer = StyledTextRenderer(string: builder.build(), contentSizeCategory: category)
logView.configure(with: renderer, width: view.bounds.width)

scrollView.contentSize = logView.bounds.size
Task {
await renderLog()
}
}

private func scrollToBottomIfNeeded() {
if wantsToScrollBottom {
scrollView.contentOffset = scrollView.maximumContentOffset
wantsToScrollBottom = false
private func renderLog() async {
do {
let category = traitCollection.preferredContentSizeCategory
let result = try await styledLogger.render(for: view.frame.width, contentSizeCategory: category)

textView.layer.contents = result.image
textView.frame = CGRect(origin: CGPoint(x: result.insets.left, y: result.insets.top), size: result.size)
} catch {
textView.layer.contents = nil
textView.frame = .zero
print(error)
}
}

@objc
func actionClearLog() {
currentText.setAttributedString(NSAttributedString())
renderLog()

scrollView.contentOffset = scrollView.minimumContentOffset
scrollView.bs.reloadBlankSlate()
}

// MARK: - FFILoggerSubscriber

func log(message: FFILogger.Message, level: FFILogger.Level) async {
let targetColor: UIColor = switch level {
case .off: .clear
case .debug: .systemIndigo
case .info: .systemBlue
case .warning: .systemOrange
case .error: .systemRed
Task {
await styledLogger.flush()
await renderLog()
}

currentText.append(NSAttributedString(string: "[\(message.target ?? "nil")] ", attributes: [
.font: UIFont.boldSystemFont(ofSize: UIFont.systemFontSize),
.foregroundColor: targetColor
]))

currentText.append(NSAttributedString(string: message.text, attributes: [
.font: UIFont.monospacedSystemFont(ofSize: UIFont.systemFontSize, weight: .regular),
.foregroundColor: UIColor.label
]))

currentText.append(NSAttributedString(string: "\n"))

wantsToScrollBottom = true
}

func flush() async {
}

// MARK: - ScrollableViewController

func scrollToInitialPosition(animated: Bool) {
wantsToScrollBottom = true
scrollToBottomIfNeeded()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class MainTabbarController: UITabBarController, UITabBarControllerDelegate {

private(set) lazy var logVC: UIViewController = {
let logMetadata = LogVCMetadata()
let logVC = LogViewController(mainModel: mainModel, metadata: logMetadata)
let logVC = LogViewController(metadata: logMetadata)
let logController = NavigationController(rootViewController: logVC)
logController.tabBarItem = logMetadata.tabbarItem

Expand Down

0 comments on commit 0aa1aaa

Please sign in to comment.