chore: profile status header and name label configure

This commit is contained in:
CMK 2021-06-17 16:31:34 +08:00
parent d9e7052cff
commit 931197e51c
12 changed files with 204 additions and 21 deletions

View File

@ -417,6 +417,7 @@
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; }; DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; };
DBAEDE5C267A058D00D25FF5 /* BlurhashImageCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */; }; DBAEDE5C267A058D00D25FF5 /* BlurhashImageCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */; };
DBAEDE5F267A0B1500D25FF5 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = DBAEDE5E267A0B1500D25FF5 /* Nuke */; }; DBAEDE5F267A0B1500D25FF5 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = DBAEDE5E267A0B1500D25FF5 /* Nuke */; };
DBAEDE61267B342D00D25FF5 /* StatusContentCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */; };
DBAFB7352645463500371D5F /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; }; DBAFB7352645463500371D5F /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; };
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; }; DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; };
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; }; DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; };
@ -989,6 +990,7 @@
DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMetricFormatter.swift; sourceTree = "<group>"; }; DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMetricFormatter.swift; sourceTree = "<group>"; };
DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = "<group>"; }; DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = "<group>"; };
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurhashImageCacheService.swift; sourceTree = "<group>"; }; DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurhashImageCacheService.swift; sourceTree = "<group>"; };
DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentCacheService.swift; sourceTree = "<group>"; };
DBAFB7342645463500371D5F /* Emojis.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emojis.swift; sourceTree = "<group>"; }; DBAFB7342645463500371D5F /* Emojis.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emojis.swift; sourceTree = "<group>"; };
DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = "<group>"; }; DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = "<group>"; };
DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = "<group>"; }; DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = "<group>"; };
@ -1358,6 +1360,7 @@
DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */, DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */,
DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */, DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */,
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */, DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */,
DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */,
); );
path = Service; path = Service;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2985,6 +2988,7 @@
DB92CF7225E7BB98002C1017 /* PollOptionTableViewCell.swift in Sources */, DB92CF7225E7BB98002C1017 /* PollOptionTableViewCell.swift in Sources */,
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */, DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */,
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */, 2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */,
DBAEDE61267B342D00D25FF5 /* StatusContentCacheService.swift in Sources */,
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */, 2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */, DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */, DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */,

View File

@ -12,12 +12,12 @@
<key>CoreDataStack.xcscheme_^#shared#^_</key> <key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>15</integer> <integer>17</integer>
</dict> </dict>
<key>Mastodon - RTL.xcscheme_^#shared#^_</key> <key>Mastodon - RTL.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>2</integer> <integer>1</integer>
</dict> </dict>
<key>Mastodon - Release.xcscheme_^#shared#^_</key> <key>Mastodon - Release.xcscheme_^#shared#^_</key>
<dict> <dict>
@ -27,7 +27,7 @@
<key>Mastodon.xcscheme_^#shared#^_</key> <key>Mastodon.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>12</integer> <integer>2</integer>
</dict> </dict>
<key>NotificationService.xcscheme_^#shared#^_</key> <key>NotificationService.xcscheme_^#shared#^_</key>
<dict> <dict>

View File

@ -194,9 +194,15 @@ extension StatusSection {
let author = (status.reblog ?? status).author let author = (status.reblog ?? status).author
return author.displayName.isEmpty ? author.username : author.displayName return author.displayName.isEmpty ? author.username : author.displayName
}() }()
cell.statusView.nameLabel.configure(content: nameText, emojiDict: (status.reblog ?? status).author.emojiDict) MastodonStatusContent.parseResult(content: nameText, emojiDict: (status.reblog ?? status).author.emojiDict)
.receive(on: DispatchQueue.main)
.sink { [weak cell] parseResult in
guard let cell = cell else { return }
cell.statusView.nameLabel.configure(contentParseResult: parseResult)
}
.store(in: &cell.disposeBag)
cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct
// set avatar // set avatar
if let reblog = status.reblog { if let reblog = status.reblog {
cell.statusView.avatarButton.isHidden = true cell.statusView.avatarButton.isHidden = true
@ -210,6 +216,19 @@ extension StatusSection {
} }
// set text // set text
// func configureStatusContent() {
// let content = (status.reblog ?? status).content
// let emojiDict = (status.reblog ?? status).emojiDict
// if let cachedParseResult = AppContext.shared.statusContentCacheService.parseResult(content: content, emojiDict: emojiDict) {
// cell.statusView.activeTextLabel.configure(contentParseResult: cachedParseResult)
// } else {
// cell.statusView.activeTextLabel.configure(
// content: (status.reblog ?? status).content,
// emojiDict: (status.reblog ?? status).emojiDict
// )
// }
// }
// configureStatusContent()
cell.statusView.activeTextLabel.configure( cell.statusView.activeTextLabel.configure(
content: (status.reblog ?? status).content, content: (status.reblog ?? status).content,
emojiDict: (status.reblog ?? status).emojiDict emojiDict: (status.reblog ?? status).emojiDict
@ -221,7 +240,7 @@ extension StatusSection {
cell.statusView.updateVisibility(visibility: visibility) cell.statusView.updateVisibility(visibility: visibility)
cell.statusView.revealContentWarningButton.publisher(for: \.isHidden) cell.statusView.revealContentWarningButton.publisher(for: \.isHidden)
.receive(on: RunLoop.main) .receive(on: DispatchQueue.main)
.sink { [weak cell] isHidden in .sink { [weak cell] isHidden in
cell?.statusView.visibilityImageView.isHidden = !isHidden cell?.statusView.visibilityImageView.isHidden = !isHidden
} }
@ -646,7 +665,13 @@ extension StatusSection {
let name = author.displayName.isEmpty ? author.username : author.displayName let name = author.displayName.isEmpty ? author.username : author.displayName
return L10n.Common.Controls.Status.userReblogged(name) return L10n.Common.Controls.Status.userReblogged(name)
}() }()
cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.author.emojiDict) MastodonStatusContent.parseResult(content: headerText, emojiDict: status.author.emojiDict)
.receive(on: DispatchQueue.main)
.sink { [weak cell] parseResult in
guard let cell = cell else { return }
cell.statusView.headerInfoLabel.configure(contentParseResult: parseResult)
}
.store(in: &cell.disposeBag)
cell.statusView.headerInfoLabel.isAccessibilityElement = true cell.statusView.headerInfoLabel.isAccessibilityElement = true
} else if status.inReplyToID != nil { } else if status.inReplyToID != nil {
cell.statusView.headerContainerView.isHidden = false cell.statusView.headerContainerView.isHidden = false
@ -659,7 +684,13 @@ extension StatusSection {
let name = author.displayName.isEmpty ? author.username : author.displayName let name = author.displayName.isEmpty ? author.username : author.displayName
return L10n.Common.Controls.Status.userRepliedTo(name) return L10n.Common.Controls.Status.userRepliedTo(name)
}() }()
cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.replyTo?.author.emojiDict ?? [:]) MastodonStatusContent.parseResult(content: headerText, emojiDict: status.replyTo?.author.emojiDict ?? [:])
.receive(on: DispatchQueue.main)
.sink { [weak cell] parseResult in
guard let cell = cell else { return }
cell.statusView.headerInfoLabel.configure(contentParseResult: parseResult)
}
.store(in: &cell.disposeBag)
cell.statusView.headerInfoLabel.isAccessibilityElement = true cell.statusView.headerInfoLabel.isAccessibilityElement = true
} else { } else {
cell.statusView.headerContainerView.isHidden = true cell.statusView.headerContainerView.isHidden = true

View File

@ -61,6 +61,7 @@ extension ActiveLabel {
} }
extension ActiveLabel { extension ActiveLabel {
/// status content /// status content
func configure(content: String, emojiDict: MastodonStatusContent.EmojiDict) { func configure(content: String, emojiDict: MastodonStatusContent.EmojiDict) {
attributedText = nil attributedText = nil
@ -76,6 +77,14 @@ extension ActiveLabel {
} }
} }
func configure(contentParseResult parseResult: MastodonStatusContent.ParseResult?) {
attributedText = nil
activeEntities.removeAll()
text = parseResult?.trimmed ?? ""
activeEntities = parseResult?.activeEntities ?? []
accessibilityLabel = parseResult?.original ?? nil
}
/// account note /// account note
func configure(note: String, emojiDict: MastodonStatusContent.EmojiDict) { func configure(note: String, emojiDict: MastodonStatusContent.EmojiDict) {
configure(content: note, emojiDict: emojiDict) configure(content: note, emojiDict: emojiDict)

View File

@ -6,6 +6,7 @@
// //
import Foundation import Foundation
import Combine
import Kanna import Kanna
import ActiveLabel import ActiveLabel
@ -14,6 +15,18 @@ enum MastodonStatusContent {
typealias EmojiShortcode = String typealias EmojiShortcode = String
typealias EmojiDict = [EmojiShortcode: URL] typealias EmojiDict = [EmojiShortcode: URL]
static let workingQueue = DispatchQueue(label: "org.joinmastodon.app.ActiveLabel.working-queue", qos: .userInteractive, attributes: .concurrent)
static func parseResult(content: String, emojiDict: MastodonStatusContent.EmojiDict) -> AnyPublisher<MastodonStatusContent.ParseResult?, Never> {
return Future { promise in
self.workingQueue.async {
let parseResult = try? MastodonStatusContent.parse(content: content, emojiDict: emojiDict)
promise(.success(parseResult))
}
}
.eraseToAnyPublisher()
}
static func parse(content: String, emojiDict: EmojiDict) throws -> MastodonStatusContent.ParseResult { static func parse(content: String, emojiDict: EmojiDict) throws -> MastodonStatusContent.ParseResult {
let document: String = { let document: String = {
var content = content var content = content
@ -113,11 +126,25 @@ extension String {
} }
extension MastodonStatusContent { extension MastodonStatusContent {
struct ParseResult { struct ParseResult: Hashable {
let document: String let document: String
let original: String let original: String
let trimmed: String let trimmed: String
let activeEntities: [ActiveEntity] let activeEntities: [ActiveEntity]
static func == (lhs: MastodonStatusContent.ParseResult, rhs: MastodonStatusContent.ParseResult) -> Bool {
return lhs.document == rhs.document
&& lhs.original == rhs.original
&& lhs.trimmed == rhs.trimmed
&& lhs.activeEntities.count == rhs.activeEntities.count // FIXME:
}
func hash(into hasher: inout Hasher) {
hasher.combine(document)
hasher.combine(original)
hasher.combine(trimmed)
hasher.combine(activeEntities.count) // FIXME:
}
} }
} }

View File

@ -33,17 +33,22 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
guard let self = self else { return } guard let self = self else { return }
for objectID in statusObjectIDs { for objectID in statusObjectIDs {
let status = backgroundManagedObjectContext.object(with: objectID) as! Status let status = backgroundManagedObjectContext.object(with: objectID) as! Status
guard let replyToID = status.inReplyToID, status.replyTo == nil else {
// skip // fetch in-reply info if needs
continue if let replyToID = status.inReplyToID, status.replyTo == nil {
self.context.statusPrefetchingService.prefetchReplyTo(
domain: domain,
statusObjectID: status.objectID,
statusID: status.id,
replyToStatusID: replyToID,
authorizationBox: activeMastodonAuthenticationBox
)
} }
self.context.statusPrefetchingService.prefetchReplyTo(
domain: domain, // self.context.statusContentCacheService.prefetch(
statusObjectID: status.objectID, // content: (status.reblog ?? status).content,
statusID: status.id, // emojiDict: (status.reblog ?? status).emojiDict
replyToStatusID: replyToID, // )
authorizationBox: activeMastodonAuthenticationBox
)
} }
} }
} }

View File

@ -15,6 +15,10 @@ import GameplayKit
import MastodonSDK import MastodonSDK
import AlamofireImage import AlamofireImage
#if DEBUG
import GDPerformanceView_Swift
#endif
final class HomeTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { final class HomeTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
@ -87,7 +91,9 @@ extension HomeTimelineViewController {
titleView.delegate = self titleView.delegate = self
viewModel.homeTimelineNavigationBarTitleViewModel.state viewModel.homeTimelineNavigationBarTitleViewModel.state
.receive(on: DispatchQueue.main) .removeDuplicates()
.debounce(for: 0.3, scheduler: RunLoop.main)
.receive(on: RunLoop.main)
.sink { [weak self] state in .sink { [weak self] state in
guard let self = self else { return } guard let self = self else { return }
self.titleView.configure(state: state) self.titleView.configure(state: state)
@ -98,6 +104,8 @@ extension HomeTimelineViewController {
#if DEBUG #if DEBUG
// long press to trigger debug menu // long press to trigger debug menu
settingBarButtonItem.menu = debugMenu settingBarButtonItem.menu = debugMenu
PerformanceMonitor.shared().delegate = self
#else #else
settingBarButtonItem.target = self settingBarButtonItem.target = self
settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:)) settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:))
@ -554,3 +562,11 @@ extension HomeTimelineViewController: StatusTableViewControllerNavigateable {
statusKeyCommandHandler(sender) statusKeyCommandHandler(sender)
} }
} }
#if DEBUG
extension HomeTimelineViewController: PerformanceMonitorDelegate {
func performanceMonitor(didReport performanceReport: PerformanceReport) {
// print(performanceReport)
}
}
#endif

View File

@ -0,0 +1,77 @@
//
// StatusContentCacheService.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-6-17.
//
import UIKit
import Combine
final class StatusContentCacheService {
var disposeBag = Set<AnyCancellable>()
let cache = NSCache<Key, ParseResultWrapper>()
let workingQueue = DispatchQueue(label: "org.joinmastodon.app.BlurhashImageCacheService.working-queue", qos: .userInitiated, attributes: .concurrent)
func parseResult(content: String, emojiDict: MastodonStatusContent.EmojiDict) -> MastodonStatusContent.ParseResult? {
let key = Key(content: content, emojiDict: emojiDict)
return cache.object(forKey: key)?.parseResult
}
func prefetch(content: String, emojiDict: MastodonStatusContent.EmojiDict) {
let key = Key(content: content, emojiDict: emojiDict)
guard cache.object(forKey: key) == nil else { return }
MastodonStatusContent.parseResult(content: content, emojiDict: emojiDict)
.sink { [weak self] parseResult in
guard let self = self else { return }
guard let parseResult = parseResult else { return }
let wrapper = ParseResultWrapper(parseResult: parseResult)
self.cache.setObject(wrapper, forKey: key)
}
.store(in: &disposeBag)
}
}
extension StatusContentCacheService {
class Key: NSObject {
let content: String
let emojiDict: MastodonStatusContent.EmojiDict
init(content: String, emojiDict: MastodonStatusContent.EmojiDict) {
self.content = content
self.emojiDict = emojiDict
}
override func isEqual(_ object: Any?) -> Bool {
guard let object = object as? Key else { return false }
return object.content == content
&& object.emojiDict == emojiDict
}
override var hash: Int {
return content.hashValue ^
emojiDict.hashValue
}
}
class ParseResultWrapper: NSObject {
let parseResult: MastodonStatusContent.ParseResult
init(parseResult: MastodonStatusContent.ParseResult) {
self.parseResult = parseResult
}
override func isEqual(_ object: Any?) -> Bool {
guard let object = object as? ParseResultWrapper else { return false }
return object.parseResult == parseResult
}
override var hash: Int {
return parseResult.hashValue
}
}
}

View File

@ -38,6 +38,7 @@ class AppContext: ObservableObject {
let placeholderImageCacheService = PlaceholderImageCacheService() let placeholderImageCacheService = PlaceholderImageCacheService()
let blurhashImageCacheService = BlurhashImageCacheService() let blurhashImageCacheService = BlurhashImageCacheService()
let statusContentCacheService = StatusContentCacheService()
let documentStore: DocumentStore let documentStore: DocumentStore
private var documentStoreSubscription: AnyCancellable! private var documentStoreSubscription: AnyCancellable!

View File

@ -10,6 +10,10 @@ import UIKit
import UserNotifications import UserNotifications
import AppShared import AppShared
#if DEBUG
import GDPerformanceView_Swift
#endif
@main @main
class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {
@ -27,6 +31,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
UNUserNotificationCenter.current().delegate = self UNUserNotificationCenter.current().delegate = self
application.registerForRemoteNotifications() application.registerForRemoteNotifications()
#if DEBUG
PerformanceMonitor.shared().start()
#endif
return true return true
} }

View File

@ -16,6 +16,7 @@ target 'Mastodon' do
# DEBUG # DEBUG
pod 'FLEX', '~> 4.4.0', :configurations => ['Debug'] pod 'FLEX', '~> 4.4.0', :configurations => ['Debug']
pod 'GDPerformanceView-Swift', '~> 2.1.1', :configurations => ['Debug']
target 'MastodonTests' do target 'MastodonTests' do
inherit! :search_paths inherit! :search_paths

View File

@ -1,6 +1,7 @@
PODS: PODS:
- DateToolsSwift (5.0.0) - DateToolsSwift (5.0.0)
- FLEX (4.4.1) - FLEX (4.4.1)
- GDPerformanceView-Swift (2.1.1)
- Kanna (5.2.4) - Kanna (5.2.4)
- Keys (1.0.1) - Keys (1.0.1)
- SwiftGen (6.4.0) - SwiftGen (6.4.0)
@ -9,6 +10,7 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- DateToolsSwift (~> 5.0.0) - DateToolsSwift (~> 5.0.0)
- FLEX (~> 4.4.0) - FLEX (~> 4.4.0)
- GDPerformanceView-Swift (~> 2.1.1)
- Kanna (~> 5.2.2) - Kanna (~> 5.2.2)
- Keys (from `Pods/CocoaPodsKeys`) - Keys (from `Pods/CocoaPodsKeys`)
- SwiftGen (~> 6.4.0) - SwiftGen (~> 6.4.0)
@ -18,6 +20,7 @@ SPEC REPOS:
trunk: trunk:
- DateToolsSwift - DateToolsSwift
- FLEX - FLEX
- GDPerformanceView-Swift
- Kanna - Kanna
- SwiftGen - SwiftGen
- "UITextField+Shake" - "UITextField+Shake"
@ -29,11 +32,12 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
DateToolsSwift: 4207ada6ad615d8dc076323d27037c94916dbfa6 DateToolsSwift: 4207ada6ad615d8dc076323d27037c94916dbfa6
FLEX: 7ca2c8cd3a435ff501ff6d2f2141e9bdc934eaab FLEX: 7ca2c8cd3a435ff501ff6d2f2141e9bdc934eaab
GDPerformanceView-Swift: 22d964fe40b19e3d914dba2586237d064de8fd77
Kanna: b9d00d7c11428308c7f95e1f1f84b8205f567a8f Kanna: b9d00d7c11428308c7f95e1f1f84b8205f567a8f
Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9 Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9
SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108
"UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3 "UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3
PODFILE CHECKSUM: 0daad1778e56099e7a7e7ebe3d292d20051840fa PODFILE CHECKSUM: 257c550231fcd1336a29f7835aa331171bb66ebd
COCOAPODS: 1.10.1 COCOAPODS: 1.10.1