From 6aff83481f277620e20f4e2e4a51f4367fe20d86 Mon Sep 17 00:00:00 2001 From: Nate Weaver Date: Wed, 15 Apr 2020 16:34:16 -0500 Subject: [PATCH] Make attributed titles work on iOS --- .../Cell/NSAttributedString+NetNewsWire.swift | 66 +++++++++++++------ NetNewsWire.xcodeproj/project.pbxproj | 2 + .../Cell/MasterTimelineCellData.swift | 3 + .../Cell/MasterTimelineTableViewCell.swift | 15 ++++- 4 files changed, 65 insertions(+), 21 deletions(-) diff --git a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift index b572001ee..576701c17 100644 --- a/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift +++ b/Mac/MainWindow/Timeline/Cell/NSAttributedString+NetNewsWire.swift @@ -6,11 +6,29 @@ // Copyright © 2020 Ranchero Software. All rights reserved. // +#if canImport(AppKit) import AppKit +typealias Font = NSFont +typealias FontDescriptor = NSFontDescriptor +typealias Color = NSColor + +private let boldTrait = NSFontDescriptor.SymbolicTraits.bold +private let italicTrait = NSFontDescriptor.SymbolicTraits.italic +private let monoSpaceTrait = NSFontDescriptor.SymbolicTraits.monoSpace +#else +import UIKit +typealias Font = UIFont +typealias FontDescriptor = UIFontDescriptor +typealias Color = UIColor + +private let boldTrait = UIFontDescriptor.SymbolicTraits.traitBold +private let italicTrait = UIFontDescriptor.SymbolicTraits.traitItalic +private let monoSpaceTrait = UIFontDescriptor.SymbolicTraits.traitMonoSpace +#endif extension NSAttributedString { - func adding(font baseFont: NSFont, color: NSColor? = nil) -> NSAttributedString { + func adding(font baseFont: Font, color: Color? = nil) -> NSAttributedString { let mutable = self.mutableCopy() as! NSMutableAttributedString let fullRange = NSRange(location: 0, length: mutable.length) @@ -22,60 +40,68 @@ extension NSAttributedString { let baseDescriptor = baseFont.fontDescriptor let baseSymbolicTraits = baseDescriptor.symbolicTraits - let baseTraits = baseDescriptor.object(forKey: .traits) as! [NSFontDescriptor.TraitKey: Any] - let baseWeight = baseTraits[.weight] as! NSFont.Weight + let baseTraits = baseDescriptor.object(forKey: .traits) as! [FontDescriptor.TraitKey: Any] + let baseWeight = baseTraits[.weight] as! Font.Weight mutable.enumerateAttribute(.font, in: fullRange, options: []) { (font: Any?, range: NSRange, stop: UnsafeMutablePointer) in - guard let font = font as? NSFont else { return } + guard let font = font as? Font else { return } var newSymbolicTraits = baseSymbolicTraits let symbolicTraits = font.fontDescriptor.symbolicTraits - if symbolicTraits.contains(.italic) { - newSymbolicTraits.insert(.italic) + if symbolicTraits.contains(italicTrait) { + newSymbolicTraits.insert(italicTrait) } - if symbolicTraits.contains(.monoSpace) { - newSymbolicTraits.insert(.monoSpace) + if symbolicTraits.contains(monoSpaceTrait) { + newSymbolicTraits.insert(monoSpaceTrait) } + #if canImport(AppKit) var descriptor = baseDescriptor.withSymbolicTraits(newSymbolicTraits) + #else + var descriptor = baseDescriptor.withSymbolicTraits(newSymbolicTraits)! + #endif - if symbolicTraits.contains(.bold) { + if symbolicTraits.contains(boldTrait) { // If the base font is semibold (as timeline titles are), make the "bold" // text heavy for better contrast. if baseWeight == .semibold { - let traits: [NSFontDescriptor.TraitKey: Any] = [.weight: NSFont.Weight.heavy] - let attributes: [NSFontDescriptor.AttributeName: Any] = [.traits: traits] + let traits: [FontDescriptor.TraitKey: Any] = [.weight: Font.Weight.heavy] + let attributes: [FontDescriptor.AttributeName: Any] = [.traits: traits] descriptor = descriptor.addingAttributes(attributes) } } - let newFont = NSFont(descriptor: descriptor, size: size) + let newFont = Font(descriptor: descriptor, size: size) mutable.addAttribute(.font, value: newFont as Any, range: range) } - // make sup/sub smaller + // make sup/sub smaller. `Key("NSSupeScript")` is used here because `.superscript` + // isn't defined in UIKit, for some reason. mutable.enumerateAttributes(in: fullRange, options: []) { (attributes: [Key : Any], range: NSRange, stop: UnsafeMutablePointer) in - guard let superscript = attributes[.superscript] as? Int else { + guard let superscript = attributes[Key("NSSuperScript")] as? Int else { return } if superscript != 0 { - let font = mutable.attribute(.font, at: range.location, effectiveRange: nil) as! NSFont + let font = mutable.attribute(.font, at: range.location, effectiveRange: nil) as! Font - let features: [NSFontDescriptor.FeatureKey: Any] = [.typeIdentifier: kVerticalPositionType, .selectorIdentifier: superscript > 0 ? kSuperiorsSelector : kInferiorsSelector] - let attributes: [NSFontDescriptor.AttributeName: Any] = [.featureSettings: [features]] + #if canImport(AppKit) + let features: [FontDescriptor.FeatureKey: Any] = [.typeIdentifier: kVerticalPositionType, .selectorIdentifier: superscript > 0 ? kSuperiorsSelector : kInferiorsSelector] + #else + let features: [FontDescriptor.FeatureKey: Any] = [.featureIdentifier: kVerticalPositionType, .typeIdentifier: superscript > 0 ? kSuperiorsSelector : kInferiorsSelector] + #endif + let attributes: [FontDescriptor.AttributeName: Any] = [.featureSettings: [features]] let descriptor = font.fontDescriptor.addingAttributes(attributes) - let newFont = NSFont(descriptor: descriptor, size: font.pointSize) + let newFont = Font(descriptor: descriptor, size: font.pointSize) mutable.addAttribute(.font, value: newFont as Any, range: range) - mutable.addAttribute(.superscript, value: 0, range: range) + mutable.addAttribute(Key("NSSuperScript"), value: 0, range: range) } - } return mutable.copy() as! NSAttributedString diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 4f38ea23b..da3069bc9 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -731,6 +731,7 @@ B2B8075E239C49D300F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; }; B2B80778239C4C7000F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; }; B2B80779239C4C7300F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; }; + B2C0FDEA2447A69100ADC150 /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B26B9571243D176B0053EEF5 /* NSAttributedString+NetNewsWire.swift */; }; B528F81E23333C7E00E735DD /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = B528F81D23333C7E00E735DD /* page.html */; }; BDCB516724282C8A00102A80 /* AccountsNewsBlur.xib in Resources */ = {isa = PBXBuildFile; fileRef = BDCB514D24282C8A00102A80 /* AccountsNewsBlur.xib */; }; BDCB516824282C8A00102A80 /* AccountsNewsBlur.xib in Resources */ = {isa = PBXBuildFile; fileRef = BDCB514D24282C8A00102A80 /* AccountsNewsBlur.xib */; }; @@ -4307,6 +4308,7 @@ 51F9F3F923DFB16300A314FD /* UITableView-Extensions.swift in Sources */, 51C452792265091600C03939 /* MasterTimelineTableViewCell.swift in Sources */, 51C4526B226508F600C03939 /* MasterFeedViewController.swift in Sources */, + B2C0FDEA2447A69100ADC150 /* NSAttributedString+NetNewsWire.swift in Sources */, 5126EE97226CB48A00C22AFC /* SceneCoordinator.swift in Sources */, 84CAFCB022BC8C35007694F0 /* FetchRequestOperation.swift in Sources */, 51EF0F77227716200050506E /* FaviconGenerator.swift in Sources */, diff --git a/iOS/MasterTimeline/Cell/MasterTimelineCellData.swift b/iOS/MasterTimeline/Cell/MasterTimelineCellData.swift index 8996654f8..d1d1c3782 100644 --- a/iOS/MasterTimeline/Cell/MasterTimelineCellData.swift +++ b/iOS/MasterTimeline/Cell/MasterTimelineCellData.swift @@ -12,6 +12,7 @@ import Articles struct MasterTimelineCellData { let title: String + let attributedTitle: NSAttributedString let summary: String let dateString: String let feedName: String @@ -28,6 +29,7 @@ struct MasterTimelineCellData { init(article: Article, showFeedName: ShowFeedName, feedName: String?, byline: String?, iconImage: IconImage?, showIcon: Bool, featuredImage: UIImage?, numberOfLines: Int, iconSize: IconSize) { self.title = ArticleStringFormatter.truncatedTitle(article) + self.attributedTitle = ArticleStringFormatter.attributedTruncatedTitle(article) self.summary = ArticleStringFormatter.truncatedSummary(article) self.dateString = ArticleStringFormatter.dateString(article.logicalDatePublished) @@ -60,6 +62,7 @@ struct MasterTimelineCellData { init() { //Empty self.title = "" + self.attributedTitle = NSAttributedString() self.summary = "" self.dateString = "" self.feedName = "" diff --git a/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift b/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift index 863ee7631..0c2d9d4a5 100644 --- a/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift +++ b/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift @@ -148,7 +148,7 @@ private extension MasterTimelineTableViewCell { func updateTitleView() { titleView.font = MasterTimelineDefaultCellLayout.titleFont titleView.textColor = labelColor - updateTextFieldText(titleView, cellData?.title) + updateTextFieldAttributedText(titleView, cellData?.attributedTitle) } func updateSummaryView() { @@ -170,6 +170,19 @@ private extension MasterTimelineTableViewCell { setNeedsLayout() } } + + func updateTextFieldAttributedText(_ label: UILabel, _ text: NSAttributedString?) { + var s = text ?? NSAttributedString(string: "") + + if let fieldFont = label.font, let color = label.textColor { + s = s.adding(font: fieldFont, color: color) + } + + if label.attributedText != s { + label.attributedText = s + setNeedsLayout() + } + } func updateFeedNameView() { switch cellData.showFeedName {