From ae34f79ca95aa09218f01eee040a097a333a3a32 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 12 Sep 2023 20:08:42 +0800 Subject: [PATCH] chore: avoid object create during scrolling --- .../TwidereUI/Content/StatusToolbarView.swift | 119 ++++++++++-------- .../Content/StatusView+ViewModel.swift | 58 +++++---- .../TwidereUI/Content/StatusView.swift | 6 +- .../xcshareddata/xcschemes/TwidereX.xcscheme | 2 - 4 files changed, 106 insertions(+), 79 deletions(-) diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift index 17992bac..2398004c 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift @@ -65,14 +65,7 @@ extension StatusToolbarView { handler(action) }, action: .reply, - image: { - switch viewModel.style { - case .inline: - return Asset.Communication.textBubbleMini.image.withRenderingMode(.alwaysTemplate) - case .plain: - return Asset.Communication.textBubble.image.withRenderingMode(.alwaysTemplate) - } - }(), + image: viewModel.replyButtonImage, count: isMetricCountDisplay ? viewModel.replyCount : nil, tintColor: nil ) @@ -83,26 +76,6 @@ extension StatusToolbarView { case repostOff case repostLock - func image(style: StatusToolbarView.Style) -> UIImage { - switch self { - case .repost: - switch style { - case .inline: return Asset.Media.repeatMini.image.withRenderingMode(.alwaysTemplate) - case .plain: return Asset.Media.repeat.image.withRenderingMode(.alwaysTemplate) - } - case .repostOff: - switch style { - case .inline: return Asset.Media.repeatOffMini.image.withRenderingMode(.alwaysTemplate) - case .plain: return Asset.Media.repeatOff.image.withRenderingMode(.alwaysTemplate) - } - case .repostLock: - switch style { - case .inline: return Asset.Media.repeatLockMini.image.withRenderingMode(.alwaysTemplate) - case .plain: return Asset.Media.repeatLock.image.withRenderingMode(.alwaysTemplate) - } - } // end switch - } // end func - static func kind( platform: Platform, isReposeRestricted: Bool, @@ -133,11 +106,12 @@ extension StatusToolbarView { }, action: .repost, image: { - return RepostButtonImage.kind( + let kind = RepostButtonImage.kind( platform: viewModel.platform, isReposeRestricted: viewModel.isReposeRestricted, isMyself: viewModel.isMyself - ).image(style: viewModel.style) + ) + return viewModel.repostButtonImage(kind: kind) }(), count: isMetricCountDisplay ? viewModel.repostCount : nil, tintColor: viewModel.isReposted ? Asset.Scene.Status.Toolbar.repost.color : nil @@ -185,15 +159,7 @@ extension StatusToolbarView { handler(action) }, action: .like, - image: { - switch viewModel.style { - case .inline: - return viewModel.isLiked ? Asset.Health.heartFillMini.image.withRenderingMode(.alwaysTemplate) : Asset.Health.heartMini.image.withRenderingMode(.alwaysTemplate) - case .plain: - return viewModel.isLiked ? Asset.Health.heartFill.image.withRenderingMode(.alwaysTemplate) : Asset.Health.heart.image.withRenderingMode(.alwaysTemplate) - } - - }(), + image: viewModel.isLiked ? viewModel.likeOnButtonImage : viewModel.likeOffButtonImage, count: isMetricCountDisplay ? viewModel.likeCount : nil, tintColor: viewModel.isLiked ? Asset.Scene.Status.Toolbar.like.color : nil ) @@ -215,15 +181,7 @@ extension StatusToolbarView { } // end ForEach } label: { HStack { - let image: UIImage = { - switch viewModel.style { - case .inline: - return Asset.Editing.ellipsisMini.image.withRenderingMode(.alwaysTemplate) - case .plain: - return Asset.Editing.ellipsis.image.withRenderingMode(.alwaysTemplate) - } - }() - Image(uiImage: image) + Image(uiImage: viewModel.moreButtonImage) .foregroundColor(.secondary) } } @@ -237,8 +195,9 @@ extension StatusToolbarView { public let menuButtonBackgroundView = UIView() // input + let style: Style + @Published var platform: Platform = .none - @Published var style: Style = .inline @Published var replyCount: Int? @Published var repostCount: Int? @@ -250,13 +209,73 @@ extension StatusToolbarView { @Published var isReposeRestricted: Bool = false @Published var isMyself: Bool = false + // output + let replyButtonImage: UIImage + let repostButtonImage: UIImage + let repostOffButtonImage: UIImage + let repostLockButtonImage: UIImage + let likeOnButtonImage: UIImage + let likeOffButtonImage: UIImage + let moreButtonImage: UIImage + var isRepostable: Bool { return isMyself || !isReposeRestricted } - public init() { + public init(style: Style) { + self.style = style + self.replyButtonImage = { + switch style { + case .inline: return Asset.Communication.textBubbleMini.image.withRenderingMode(.alwaysTemplate) + case .plain: return Asset.Communication.textBubble.image.withRenderingMode(.alwaysTemplate) + } + }() + self.repostButtonImage = { + switch style { + case .inline: return Asset.Media.repeatMini.image.withRenderingMode(.alwaysTemplate) + case .plain: return Asset.Media.repeat.image.withRenderingMode(.alwaysTemplate) + } + }() + self.repostOffButtonImage = { + switch style { + case .inline: return Asset.Media.repeatOffMini.image.withRenderingMode(.alwaysTemplate) + case .plain: return Asset.Media.repeatOff.image.withRenderingMode(.alwaysTemplate) + } + }() + self.repostLockButtonImage = { + switch style { + case .inline: return Asset.Media.repeatLockMini.image.withRenderingMode(.alwaysTemplate) + case .plain: return Asset.Media.repeatLock.image.withRenderingMode(.alwaysTemplate) + } + }() + self.likeOnButtonImage = { + switch style { + case .inline: return Asset.Health.heartFillMini.image.withRenderingMode(.alwaysTemplate) + case .plain: return Asset.Health.heartFill.image.withRenderingMode(.alwaysTemplate) + } + }() + self.likeOffButtonImage = { + switch style { + case .inline: return Asset.Health.heartMini.image.withRenderingMode(.alwaysTemplate) + case .plain: return Asset.Health.heart.image.withRenderingMode(.alwaysTemplate) + } + }() + self.moreButtonImage = { + switch style { + case .inline: return Asset.Editing.ellipsisMini.image.withRenderingMode(.alwaysTemplate) + case .plain: return Asset.Editing.ellipsis.image.withRenderingMode(.alwaysTemplate) + } + }() // end init } + + func repostButtonImage(kind: RepostButtonImage) -> UIImage { + switch kind { + case .repost: return repostButtonImage + case .repostOff: return repostOffButtonImage + case .repostLock: return repostLockButtonImage + } + } } } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index 3d6f022a..e0ee3748 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -65,8 +65,11 @@ extension StatusView { // content @Published public var spoilerContent: MetaContent? + @Published public var spoilerContentAttributedString: AttributedString? @Published public var isSpoilerContentContainsMeta: Bool = false + @Published public var content: MetaContent = PlaintextMetaContent(string: "") + @Published public var contentAttributedString = AttributedString("") @Published public var isContentContainsMeta: Bool = false var isContentEmpty: Bool { content.string.isEmpty } @@ -122,23 +125,7 @@ extension StatusView { // visibility @Published public var visibility: MastodonVisibility? - var visibilityIconImage: UIImage? { - switch visibility { - case .public: - return Asset.ObjectTools.globeMiniInline.image.withRenderingMode(.alwaysTemplate) - case .unlisted: - return Asset.ObjectTools.lockOpenMiniInline.image.withRenderingMode(.alwaysTemplate) - case .private: - return Asset.ObjectTools.lockMiniInline.image.withRenderingMode(.alwaysTemplate) - case .direct: - return Asset.Communication.mailMiniInline.image.withRenderingMode(.alwaysTemplate) - case ._other: - assertionFailure() - return nil - case nil: - return nil - } - } + @Published public var visibilityIconImage: UIImage? // @Published public var groupedAccessibilityLabel = "" @@ -152,7 +139,7 @@ extension StatusView { @Published public var metricViewModel: StatusMetricView.ViewModel? // toolbar - public let toolbarViewModel = StatusToolbarView.ViewModel() + public let toolbarViewModel: StatusToolbarView.ViewModel public var canDelete: Bool { guard let authContext = self.authContext else { return false } guard let authorUserIdentifier = self.authorUserIdentifier else { return false } @@ -192,6 +179,7 @@ extension StatusView { guard let myUserIdentifier = authContext?.authenticationContext.userIdentifier else { return false } return myUserIdentifier == _authorUserIdentifier }() + self.toolbarViewModel = StatusToolbarView.ViewModel(style: kind == .conversationRoot ? .plain : .inline) // end init viewLayoutFramePublisher?.assign(to: &$viewLayoutFrame) @@ -208,6 +196,7 @@ extension StatusView { self.kind = .timeline self.authorUserIdentifier = nil self.isMyself = false + self.toolbarViewModel = StatusToolbarView.ViewModel(style: kind == .conversationRoot ? .plain : .inline) // end init viewLayoutFramePublisher?.assign(to: &$viewLayoutFrame) @@ -224,9 +213,6 @@ extension StatusView { UserDefaults.shared.publisher(for: \.translateButtonPreference) .map { $0 } .assign(to: &$translateButtonPreference) - - // toolbar - toolbarViewModel.style = kind == .conversationRoot ? .plain : .inline } } } @@ -248,6 +234,7 @@ extension StatusView.ViewModel { useParagraphMark: true ) self.content = metaContent + self.contentAttributedString = metaContent.attributedString(accentColor: .tintColor) // delegate?.statusView(self, translateContentDidChange: status) } catch { debugPrint(error.localizedDescription) @@ -503,6 +490,7 @@ extension StatusView.ViewModel { useParagraphMark: true ) self.content = metaContent + self.contentAttributedString = metaContent.attributedString(accentColor: .tintColor) self.isContentContainsMeta = false } @@ -653,8 +641,24 @@ extension StatusView.ViewModel { .assign(to: &$protected) // visibility - visibility = status.visibility - + let _visibility = status.visibility + visibility = _visibility + visibilityIconImage = { + switch _visibility { + case .public: + return Asset.ObjectTools.globeMiniInline.image.withRenderingMode(.alwaysTemplate) + case .unlisted: + return Asset.ObjectTools.lockOpenMiniInline.image.withRenderingMode(.alwaysTemplate) + case .private: + return Asset.ObjectTools.lockMiniInline.image.withRenderingMode(.alwaysTemplate) + case .direct: + return Asset.Communication.mailMiniInline.image.withRenderingMode(.alwaysTemplate) + case ._other: + assertionFailure() + return nil + } + }() + // timestamp timestampLabelViewModel = TimestampLabelView.ViewModel(timestamp: status.createdAt) @@ -664,10 +668,12 @@ extension StatusView.ViewModel { let content = MastodonContent(content: spoilerText, emojis: status.emojisTransient.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content, useParagraphMark: true) self.spoilerContent = metaContent + self.spoilerContentAttributedString = metaContent.attributedString(accentColor: .tintColor) self.isSpoilerContentContainsMeta = !status.emojisTransient.isEmpty } catch { assertionFailure(error.localizedDescription) self.spoilerContent = nil + self.spoilerContentAttributedString = nil } } @@ -676,10 +682,12 @@ extension StatusView.ViewModel { let content = MastodonContent(content: status.content, emojis: status.emojisTransient.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content, useParagraphMark: true) self.content = metaContent + self.contentAttributedString = metaContent.attributedString(accentColor: .tintColor) self.isContentContainsMeta = !status.emojisTransient.isEmpty } catch { assertionFailure(error.localizedDescription) self.content = PlaintextMetaContent(string: "") + self.contentAttributedString = AttributedString("") } // language @@ -783,11 +791,13 @@ extension StatusView.ViewModel { viewModel.avatarURL = URL(string: "https://pbs.twimg.com/profile_images/809741368134234112/htSiXXAU_400x400.jpg") viewModel.authorName = PlaintextMetaContent(string: "Twidere") viewModel.authorUsernme = "TwidereProject" - viewModel.content = TwitterMetaContent.convert( + let metaContent = TwitterMetaContent.convert( document: TwitterContent(content: L10n.Scene.Settings.Display.Preview.thankForUsingTwidereX, urlEntities: []), urlMaximumLength: 16, twitterTextProvider: SwiftTwitterTextProvider() ) + viewModel.content = metaContent + viewModel.contentAttributedString = metaContent.attributedString(accentColor: .tintColor) return viewModel } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index c74114ea..6db7a9b9 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -449,8 +449,8 @@ extension StatusView { // ignore tap } } else { - let metaContent = viewModel.spoilerContent ?? PlaintextMetaContent(string: "") - Text(metaContent.attributedString(accentColor: .tintColor)) + let metaContent = viewModel.spoilerContentAttributedString ?? AttributedString("") + Text(metaContent) .multilineTextAlignment(.leading) .font(Font(TextStyle.statusContent.font)) .foregroundColor(Color(uiColor: TextStyle.statusContent.textColor)) @@ -474,7 +474,7 @@ extension StatusView { // ignore tap } } else { - Text(viewModel.content.attributedString(accentColor: .tintColor)) + Text(viewModel.contentAttributedString) .multilineTextAlignment(.leading) .font(Font(TextStyle.statusContent.font)) .foregroundColor(Color(uiColor: TextStyle.statusContent.textColor)) diff --git a/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX.xcscheme b/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX.xcscheme index 0f717735..a1237242 100644 --- a/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX.xcscheme +++ b/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX.xcscheme @@ -73,8 +73,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - enableThreadSanitizer = "YES" - enableUBSanitizer = "YES" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO"