From 9051e5d1ec297573dcb60b3dec703e6205d93b5e Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 8 Feb 2022 18:17:17 +0800 Subject: [PATCH] feat: update setting scene UI --- Localization/app.json | 7 + Mastodon.xcodeproj/project.pbxproj | 8 +- .../Diffiable/Settings/SettingsItem.swift | 24 +-- .../Diffiable/Settings/SettingsSection.swift | 69 +++---- ...ngsAppearanceTableViewCell+ViewModel.swift | 74 ++++++++ .../SettingsAppearanceTableViewCell.swift | 148 +++++++++++++++ .../Cell/SettingsLinkTableViewCell.swift | 0 .../Cell/SettingsToggleTableViewCell.swift | 1 + .../Settings/SettingsViewController.swift | 81 ++++---- .../Scene/Settings/SettingsViewModel.swift | 20 +- .../Scene/Settings/View/AppearanceView.swift | 70 ++++--- .../SettingsAppearanceTableViewCell.swift | 173 ------------------ 12 files changed, 379 insertions(+), 296 deletions(-) create mode 100644 Mastodon/Scene/Settings/Cell/SettingsAppearanceTableViewCell+ViewModel.swift create mode 100644 Mastodon/Scene/Settings/Cell/SettingsAppearanceTableViewCell.swift rename Mastodon/Scene/Settings/{View => }/Cell/SettingsLinkTableViewCell.swift (100%) rename Mastodon/Scene/Settings/{View => }/Cell/SettingsToggleTableViewCell.swift (96%) delete mode 100644 Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift diff --git a/Localization/app.json b/Localization/app.json index e57192a4..78171749 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -514,6 +514,13 @@ "light": "Always Light", "dark": "Always Dark" }, + "look_and_feel": { + "title": "Look and Feel", + "use_system": "Use System", + "really_dark": "Really Dark", + "sorta_dark": "Sorta Dark", + "light": "Light" + }, "notifications": { "title": "Notifications", "favorites": "Favorites my post", diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index e51739a2..542636a4 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -446,6 +446,7 @@ DB98EB6527B216500082E365 /* ReportResultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6427B216500082E365 /* ReportResultViewModel.swift */; }; DB98EB6727B216560082E365 /* ReportResultViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6627B216560082E365 /* ReportResultViewModel+Diffable.swift */; }; DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */; }; + DB98EB6B27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6A27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift */; }; DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */; }; DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */ = {isa = PBXBuildFile; productRef = DB9A487D2603456B008B817C /* UITextView+Placeholder */; }; DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */; }; @@ -1187,6 +1188,7 @@ DB98EB6427B216500082E365 /* ReportResultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultViewModel.swift; sourceTree = ""; }; DB98EB6627B216560082E365 /* ReportResultViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportResultViewModel+Diffable.swift"; sourceTree = ""; }; DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultActionTableViewCell.swift; sourceTree = ""; }; + DB98EB6A27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsAppearanceTableViewCell+ViewModel.swift"; sourceTree = ""; }; DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentContainerView+EmptyStateView.swift"; sourceTree = ""; }; DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+PublishState.swift"; sourceTree = ""; }; DB9A488F26035963008B817C /* APIService+Media.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Media.swift"; sourceTree = ""; }; @@ -1831,6 +1833,7 @@ 5B90C455262599800002E742 /* Settings */ = { isa = PBXGroup; children = ( + 5B90C458262599800002E742 /* Cell */, 5B90C457262599800002E742 /* View */, DB6D9F9626367249008423CD /* SettingsViewController.swift */, 5B90C456262599800002E742 /* SettingsViewModel.swift */, @@ -1841,7 +1844,6 @@ 5B90C457262599800002E742 /* View */ = { isa = PBXGroup; children = ( - 5B90C458262599800002E742 /* Cell */, 5B90C45C262599800002E742 /* SettingsSectionHeader.swift */, DB443CD32694627B00159B29 /* AppearanceView.swift */, ); @@ -1851,8 +1853,9 @@ 5B90C458262599800002E742 /* Cell */ = { isa = PBXGroup; children = ( - 5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */, 5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */, + DB98EB6A27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift */, + 5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */, 5B90C45B262599800002E742 /* SettingsLinkTableViewCell.swift */, ); path = Cell; @@ -4196,6 +4199,7 @@ DB6B7500272FF73800C70B6E /* UserTableViewCell.swift in Sources */, DB1D842E26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift in Sources */, DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */, + DB98EB6B27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift in Sources */, DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */, DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */, DB4932B926F31AD300EF46D4 /* BadgeButton.swift in Sources */, diff --git a/Mastodon/Diffiable/Settings/SettingsItem.swift b/Mastodon/Diffiable/Settings/SettingsItem.swift index 99c956e7..50f0a761 100644 --- a/Mastodon/Diffiable/Settings/SettingsItem.swift +++ b/Mastodon/Diffiable/Settings/SettingsItem.swift @@ -7,13 +7,14 @@ import UIKit import CoreData +import CoreDataStack import MastodonAsset import MastodonLocalization enum SettingsItem { - case appearance(settingObjectID: NSManagedObjectID) - case notification(settingObjectID: NSManagedObjectID, switchMode: NotificationSwitchMode) - case preference(settingObjectID: NSManagedObjectID, preferenceType: PreferenceType) + case appearance(record: ManagedObjectRecord) + case preference(settingRecord: ManagedObjectRecord, preferenceType: PreferenceType) + case notification(settingRecord: ManagedObjectRecord, switchMode: NotificationSwitchMode) case boringZone(item: Link) case spicyZone(item: Link) } @@ -21,9 +22,10 @@ enum SettingsItem { extension SettingsItem { enum AppearanceMode: String { - case automatic + case system + case reallyDark + case sortaDark case light - case dark } enum NotificationSwitchMode: CaseIterable, Hashable { @@ -43,14 +45,12 @@ extension SettingsItem { } enum PreferenceType: CaseIterable { - case darkMode case disableAvatarAnimation case disableEmojiAnimation case useDefaultBrowser var title: String { switch self { - case .darkMode: return L10n.Scene.Settings.Section.Preference.trueBlackDarkMode case .disableAvatarAnimation: return L10n.Scene.Settings.Section.Preference.disableAvatarAnimation case .disableEmojiAnimation: return L10n.Scene.Settings.Section.Preference.disableEmojiAnimation case .useDefaultBrowser: return L10n.Scene.Settings.Section.Preference.usingDefaultBrowser @@ -77,12 +77,12 @@ extension SettingsItem { } } - var textColor: UIColor { + var textColor: UIColor? { switch self { - case .accountSettings: return Asset.Colors.brandBlue.color - case .github: return Asset.Colors.brandBlue.color - case .termsOfService: return Asset.Colors.brandBlue.color - case .privacyPolicy: return Asset.Colors.brandBlue.color + case .accountSettings: return nil // tintColor + case .github: return nil + case .termsOfService: return nil + case .privacyPolicy: return nil case .clearMediaCache: return .systemRed case .signOut: return .systemRed } diff --git a/Mastodon/Diffiable/Settings/SettingsSection.swift b/Mastodon/Diffiable/Settings/SettingsSection.swift index ab0ec4e8..cc03ae05 100644 --- a/Mastodon/Diffiable/Settings/SettingsSection.swift +++ b/Mastodon/Diffiable/Settings/SettingsSection.swift @@ -13,16 +13,16 @@ import MastodonLocalization enum SettingsSection: Hashable { case appearance - case notifications case preference + case notifications case boringZone case spicyZone var title: String { switch self { - case .appearance: return L10n.Scene.Settings.Section.Appearance.title + case .appearance: return "Look and Feel" // TODO: i18n + case .preference: return "" case .notifications: return L10n.Scene.Settings.Section.Notifications.title - case .preference: return L10n.Scene.Settings.Section.Preference.title case .boringZone: return L10n.Scene.Settings.Section.BoringZone.title case .spicyZone: return L10n.Scene.Settings.Section.SpicyZone.title } @@ -41,25 +41,38 @@ extension SettingsSection { weak settingsToggleCellDelegate ] tableView, indexPath, item -> UITableViewCell? in switch item { - case .appearance(let objectID): + case .appearance(let record): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsAppearanceTableViewCell.self), for: indexPath) as! SettingsAppearanceTableViewCell - UserDefaults.shared.observe(\.customUserInterfaceStyle, options: [.initial, .new]) { [weak cell] defaults, _ in - guard let cell = cell else { return } - switch defaults.customUserInterfaceStyle { - case .unspecified: cell.update(with: .automatic) - case .dark: cell.update(with: .dark) - case .light: cell.update(with: .light) - @unknown default: - assertionFailure() - } + managedObjectContext.performAndWait { + guard let setting = record.object(in: managedObjectContext) else { return } + cell.configure(setting: setting) } - .store(in: &cell.observations) cell.delegate = settingsAppearanceTableViewCellDelegate return cell - case .notification(let objectID, let switchMode): + case .preference(let record, _): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsToggleTableViewCell.self), for: indexPath) as! SettingsToggleTableViewCell + cell.delegate = settingsToggleCellDelegate + managedObjectContext.performAndWait { + guard let setting = record.object(in: managedObjectContext) else { return } + SettingsSection.configureSettingToggle(cell: cell, item: item, setting: setting) + + ManagedObjectObserver.observe(object: setting) + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { _ in + // do nothing + }, receiveValue: { [weak cell] change in + guard let cell = cell else { return } + guard case .update(let object) = change.changeType, + let setting = object as? Setting else { return } + SettingsSection.configureSettingToggle(cell: cell, item: item, setting: setting) + }) + .store(in: &cell.disposeBag) + } + return cell + case .notification(let record, let switchMode): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsToggleTableViewCell.self), for: indexPath) as! SettingsToggleTableViewCell managedObjectContext.performAndWait { - let setting = managedObjectContext.object(with: objectID) as! Setting + guard let setting = record.object(in: managedObjectContext) else { return } if let subscription = setting.activeSubscription { SettingsSection.configureSettingToggle(cell: cell, switchMode: switchMode, subscription: subscription) } @@ -77,32 +90,12 @@ extension SettingsSection { } cell.delegate = settingsToggleCellDelegate return cell - case .preference(let objectID, _): - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsToggleTableViewCell.self), for: indexPath) as! SettingsToggleTableViewCell - cell.delegate = settingsToggleCellDelegate - managedObjectContext.performAndWait { - let setting = managedObjectContext.object(with: objectID) as! Setting - SettingsSection.configureSettingToggle(cell: cell, item: item, setting: setting) - - ManagedObjectObserver.observe(object: setting) - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { _ in - // do nothing - }, receiveValue: { [weak cell] change in - guard let cell = cell else { return } - guard case .update(let object) = change.changeType, - let setting = object as? Setting else { return } - SettingsSection.configureSettingToggle(cell: cell, item: item, setting: setting) - }) - .store(in: &cell.disposeBag) - } - return cell case .boringZone(let item), .spicyZone(let item): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsLinkTableViewCell.self), for: indexPath) as! SettingsLinkTableViewCell cell.update(with: item) return cell - } + } // end switch } } } @@ -119,8 +112,6 @@ extension SettingsSection { cell.textLabel?.text = preferenceType.title switch preferenceType { - case .darkMode: - cell.switchButton.isOn = setting.preferredTrueBlackDarkMode case .disableAvatarAnimation: cell.switchButton.isOn = setting.preferredStaticAvatar case .disableEmojiAnimation: diff --git a/Mastodon/Scene/Settings/Cell/SettingsAppearanceTableViewCell+ViewModel.swift b/Mastodon/Scene/Settings/Cell/SettingsAppearanceTableViewCell+ViewModel.swift new file mode 100644 index 00000000..ea589da9 --- /dev/null +++ b/Mastodon/Scene/Settings/Cell/SettingsAppearanceTableViewCell+ViewModel.swift @@ -0,0 +1,74 @@ +// +// SettingsAppearanceTableViewCell+ViewModel.swift +// Mastodon +// +// Created by MainasuK on 2022-2-8. +// + +import UIKit +import Combine +import CoreDataStack + +extension SettingsAppearanceTableViewCell { + final class ViewModel: ObservableObject { + var disposeBag = Set() + private var observations = Set() + + // input + @Published public var customUserInterfaceStyle: UIUserInterfaceStyle = .unspecified + @Published public var preferredTrueBlackDarkMode = false + // output + @Published public var appearanceMode: SettingsItem.AppearanceMode = .system + + init() { + UserDefaults.shared.observe(\.customUserInterfaceStyle, options: [.initial, .new]) { [weak self] defaults, _ in + guard let self = self else { return } + self.customUserInterfaceStyle = defaults.customUserInterfaceStyle + } + .store(in: &observations) + } + + public func prepareForReuse() { + // do nothing + } + } +} + +extension SettingsAppearanceTableViewCell.ViewModel { + func bind(cell: SettingsAppearanceTableViewCell) { + Publishers.CombineLatest( + $customUserInterfaceStyle, + $preferredTrueBlackDarkMode + ) + .sink { customUserInterfaceStyle, preferredTrueBlackDarkMode in + cell.appearanceViews.forEach { view in + view.selected = false + } + + switch customUserInterfaceStyle { + case .unspecified: + cell.systemAppearanceView.selected = true + case .dark: + if preferredTrueBlackDarkMode { + cell.reallyDarkAppearanceView.selected = true + } else { + cell.sortaDarkAppearanceView.selected = true + } + case .light: + cell.lightAppearanceView.selected = true + @unknown default: + assertionFailure() + } + } + .store(in: &disposeBag) + } +} + +extension SettingsAppearanceTableViewCell { + func configure(setting: Setting) { + setting.publisher(for: \.preferredTrueBlackDarkMode) + .assign(to: \.preferredTrueBlackDarkMode, on: viewModel) + .store(in: &disposeBag) + } + +} diff --git a/Mastodon/Scene/Settings/Cell/SettingsAppearanceTableViewCell.swift b/Mastodon/Scene/Settings/Cell/SettingsAppearanceTableViewCell.swift new file mode 100644 index 00000000..b808d1c4 --- /dev/null +++ b/Mastodon/Scene/Settings/Cell/SettingsAppearanceTableViewCell.swift @@ -0,0 +1,148 @@ +// +// SettingsAppearanceTableViewCell.swift +// Mastodon +// +// Created by ihugo on 2021/4/8. +// + +import UIKit +import Combine +import MastodonAsset +import MastodonLocalization + +protocol SettingsAppearanceTableViewCellDelegate: AnyObject { + func settingsAppearanceTableViewCell(_ cell: SettingsAppearanceTableViewCell, didSelectAppearanceMode appearanceMode: SettingsItem.AppearanceMode) +} + +class SettingsAppearanceTableViewCell: UITableViewCell { + + var disposeBag = Set() + var observations = Set() + + static let spacing: CGFloat = 28 + + weak var delegate: SettingsAppearanceTableViewCellDelegate? + + public private(set) lazy var viewModel: ViewModel = { + let viewModel = ViewModel() + viewModel.bind(cell: self) + return viewModel + }() + + lazy var stackView: UIStackView = { + let view = UIStackView() + view.axis = .horizontal + view.distribution = .fillEqually + view.spacing = SettingsAppearanceTableViewCell.spacing + return view + }() + + let systemAppearanceView = AppearanceView( + image: Asset.Settings.darkAuto.image, + title: "Use System" // TODO: i18n + ) + let reallyDarkAppearanceView = AppearanceView( + image: Asset.Settings.dark.image, + title: "Really Dark" + ) + let sortaDarkAppearanceView = AppearanceView( + image: Asset.Settings.dark.image, + title: "Sorta Dark" + ) + let lightAppearanceView = AppearanceView( + image: Asset.Settings.light.image, + title: "Light" + ) + + var appearanceViews: [AppearanceView] { + return [ + systemAppearanceView, + reallyDarkAppearanceView, + sortaDarkAppearanceView, + lightAppearanceView, + ] + } + + override func prepareForReuse() { + super.prepareForReuse() + + disposeBag.removeAll() + observations.removeAll() + viewModel.prepareForReuse() + } + + // MARK: - Methods + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + // remove separator line in section of group tableview + for subview in self.subviews { + if subview != self.contentView && subview.frame.width == self.frame.width { + subview.removeFromSuperview() + } + } + } + +} + +extension SettingsAppearanceTableViewCell { + + // MARK: Private methods + private func setupUI() { + backgroundColor = .clear + selectionStyle = .none + + stackView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(stackView) + NSLayoutConstraint.activate([ + stackView.topAnchor.constraint(equalTo: contentView.topAnchor), + stackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + stackView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + ]) + + stackView.addArrangedSubview(systemAppearanceView) + stackView.addArrangedSubview(reallyDarkAppearanceView) + stackView.addArrangedSubview(sortaDarkAppearanceView) + stackView.addArrangedSubview(lightAppearanceView) + + appearanceViews.forEach { view in + let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer + view.addGestureRecognizer(tapGestureRecognizer) + tapGestureRecognizer.addTarget(self, action: #selector(SettingsAppearanceTableViewCell.appearanceViewDidPressed(_:))) + } + } + +} + +// MARK: - Actions +extension SettingsAppearanceTableViewCell { + @objc func appearanceViewDidPressed(_ sender: UITapGestureRecognizer) { + let mode: SettingsItem.AppearanceMode + + switch sender.view { + case systemAppearanceView: + mode = .system + case reallyDarkAppearanceView: + mode = .reallyDark + case sortaDarkAppearanceView: + mode = .sortaDark + case lightAppearanceView: + mode = .light + default: + assertionFailure() + return + } + + delegate?.settingsAppearanceTableViewCell(self, didSelectAppearanceMode: mode) + } +} diff --git a/Mastodon/Scene/Settings/View/Cell/SettingsLinkTableViewCell.swift b/Mastodon/Scene/Settings/Cell/SettingsLinkTableViewCell.swift similarity index 100% rename from Mastodon/Scene/Settings/View/Cell/SettingsLinkTableViewCell.swift rename to Mastodon/Scene/Settings/Cell/SettingsLinkTableViewCell.swift diff --git a/Mastodon/Scene/Settings/View/Cell/SettingsToggleTableViewCell.swift b/Mastodon/Scene/Settings/Cell/SettingsToggleTableViewCell.swift similarity index 96% rename from Mastodon/Scene/Settings/View/Cell/SettingsToggleTableViewCell.swift rename to Mastodon/Scene/Settings/Cell/SettingsToggleTableViewCell.swift index e75fa831..419f24d6 100644 --- a/Mastodon/Scene/Settings/View/Cell/SettingsToggleTableViewCell.swift +++ b/Mastodon/Scene/Settings/Cell/SettingsToggleTableViewCell.swift @@ -20,6 +20,7 @@ class SettingsToggleTableViewCell: UITableViewCell { private(set) lazy var switchButton: UISwitch = { let view = UISwitch(frame:.zero) + view.onTintColor = contentView.window?.tintColor ?? .label return view }() diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift index 9352603e..cf20e2df 100644 --- a/Mastodon/Scene/Settings/SettingsViewController.swift +++ b/Mastodon/Scene/Settings/SettingsViewController.swift @@ -99,15 +99,13 @@ class SettingsViewController: UIViewController, NeedsDependency { }() private(set) lazy var tableView: UITableView = { - // init with a frame to fix a conflict ('UIView-Encapsulated-Layout-Width' UIStackView:0x7f8c2b6c0590.width == 0) let style: UITableView.Style = { switch UIDevice.current.userInterfaceIdiom { - case .phone: - return .grouped - default: - return .insetGrouped + case .phone: return .grouped + default: return .insetGrouped } }() + // init with a frame to fix a conflict ('UIView-Encapsulated-Layout-Width' UIStackView:0x7f8c2b6c0590.width == 0) let tableView = UITableView(frame: CGRect(x: 0, y: 0, width: 320, height: 320), style: style) tableView.translatesAutoresizingMaskIntoConstraints = false tableView.delegate = self @@ -135,6 +133,15 @@ class SettingsViewController: UIViewController, NeedsDependency { return view }() + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension SettingsViewController { + override func viewDidLoad() { super.viewDidLoad() @@ -214,7 +221,7 @@ class SettingsViewController: UIViewController, NeedsDependency { private func setupView() { setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) ThemeService.shared.currentTheme - .receive(on: RunLoop.main) + .receive(on: DispatchQueue.main) .sink { [weak self] theme in guard let self = self else { return } self.setupBackgroundColor(theme: theme) @@ -314,10 +321,6 @@ class SettingsViewController: UIViewController, NeedsDependency { .store(in: &disposeBag) } - deinit { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function) - } - } // Mark: - Actions @@ -327,7 +330,9 @@ extension SettingsViewController { } } +// MARK: - UITableViewDelegate extension SettingsViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { let sections = viewModel.dataSource.snapshot().sectionIdentifiers guard section < sections.count else { return nil } @@ -449,24 +454,42 @@ extension SettingsViewController { // MARK: - SettingsAppearanceTableViewCellDelegate extension SettingsViewController: SettingsAppearanceTableViewCellDelegate { - func settingsAppearanceCell(_ cell: SettingsAppearanceTableViewCell, didSelectAppearanceMode appearanceMode: SettingsItem.AppearanceMode) { + func settingsAppearanceTableViewCell( + _ cell: SettingsAppearanceTableViewCell, + didSelectAppearanceMode appearanceMode: SettingsItem.AppearanceMode + ) { guard let dataSource = viewModel.dataSource else { return } guard let indexPath = tableView.indexPath(for: cell) else { return } let item = dataSource.itemIdentifier(for: indexPath) - guard case .appearance = item else { return } - - switch appearanceMode { - case .automatic: - UserDefaults.shared.customUserInterfaceStyle = .unspecified - case .light: - UserDefaults.shared.customUserInterfaceStyle = .light - case .dark: - UserDefaults.shared.customUserInterfaceStyle = .dark - } + guard case let .appearance(record) = item else { return } - let feedbackGenerator = UIImpactFeedbackGenerator(style: .light) - feedbackGenerator.impactOccurred() + Task { @MainActor in + var preferredTrueBlackDarkMode = false + + switch appearanceMode { + case .system: + UserDefaults.shared.customUserInterfaceStyle = .unspecified + case .reallyDark: + UserDefaults.shared.customUserInterfaceStyle = .dark + preferredTrueBlackDarkMode = true + case .sortaDark: + UserDefaults.shared.customUserInterfaceStyle = .dark + case .light: + UserDefaults.shared.customUserInterfaceStyle = .light + } + + let managedObjectContext = context.managedObjectContext + try await managedObjectContext.performChanges { + guard let setting = record.object(in: managedObjectContext) else { return } + setting.update(preferredTrueBlackDarkMode: preferredTrueBlackDarkMode) + } + ThemeService.shared.set(themeName: preferredTrueBlackDarkMode ? .system : .mastodon) + + let feedbackGenerator = UIImpactFeedbackGenerator(style: .light) + feedbackGenerator.impactOccurred() + } // end Task } + } extension SettingsViewController: SettingsToggleCellDelegate { @@ -478,10 +501,10 @@ extension SettingsViewController: SettingsToggleCellDelegate { let item = dataSource.itemIdentifier(for: indexPath) switch item { - case .notification(let settingObjectID, let switchMode): + case .notification(let record, let switchMode): let managedObjectContext = context.backgroundManagedObjectContext managedObjectContext.performChanges { - let setting = managedObjectContext.object(with: settingObjectID) as! Setting + guard let setting = record.object(in: managedObjectContext) else { return } guard let subscription = setting.activeSubscription else { return } let alert = subscription.alert switch switchMode { @@ -497,13 +520,11 @@ extension SettingsViewController: SettingsToggleCellDelegate { // do nothing } .store(in: &disposeBag) - case .preference(let settingObjectID, let preferenceType): + case .preference(let record, let preferenceType): let managedObjectContext = context.backgroundManagedObjectContext managedObjectContext.performChanges { - let setting = managedObjectContext.object(with: settingObjectID) as! Setting + guard let setting = record.object(in: managedObjectContext) else { return } switch preferenceType { - case .darkMode: - setting.update(preferredTrueBlackDarkMode: isOn) case .disableAvatarAnimation: setting.update(preferredStaticAvatar: isOn) case .disableEmojiAnimation: @@ -516,8 +537,6 @@ extension SettingsViewController: SettingsToggleCellDelegate { switch result { case .success: switch preferenceType { - case .darkMode: - ThemeService.shared.set(themeName: isOn ? .system : .mastodon) case .disableAvatarAnimation: UserDefaults.shared.preferredStaticAvatar = isOn case .disableEmojiAnimation: diff --git a/Mastodon/Scene/Settings/SettingsViewModel.swift b/Mastodon/Scene/Settings/SettingsViewModel.swift index 9158e816..9f9640b7 100644 --- a/Mastodon/Scene/Settings/SettingsViewModel.swift +++ b/Mastodon/Scene/Settings/SettingsViewModel.swift @@ -108,24 +108,24 @@ extension SettingsViewModel { var snapshot = NSDiffableDataSourceSnapshot() // appearance - let appearanceItems = [SettingsItem.appearance(settingObjectID: setting.objectID)] + let appearanceItems = [SettingsItem.appearance(record: .init(objectID: setting.objectID))] snapshot.appendSections([.appearance]) snapshot.appendItems(appearanceItems, toSection: .appearance) - - // notification - let notificationItems = SettingsItem.NotificationSwitchMode.allCases.map { mode in - SettingsItem.notification(settingObjectID: setting.objectID, switchMode: mode) - } - snapshot.appendSections([.notifications]) - snapshot.appendItems(notificationItems, toSection: .notifications) - + // preference snapshot.appendSections([.preference]) let preferenceItems: [SettingsItem] = SettingsItem.PreferenceType.allCases.map { preferenceType in - SettingsItem.preference(settingObjectID: setting.objectID, preferenceType: preferenceType) + SettingsItem.preference(settingRecord: .init(objectID: setting.objectID), preferenceType: preferenceType) } snapshot.appendItems(preferenceItems,toSection: .preference) + // notification + let notificationItems = SettingsItem.NotificationSwitchMode.allCases.map { mode in + SettingsItem.notification(settingRecord: .init(objectID: setting.objectID), switchMode: mode) + } + snapshot.appendSections([.notifications]) + snapshot.appendItems(notificationItems, toSection: .notifications) + // boring zone let boringZoneSettingsItems: [SettingsItem] = { let links: [SettingsItem.Link] = [ diff --git a/Mastodon/Scene/Settings/View/AppearanceView.swift b/Mastodon/Scene/Settings/View/AppearanceView.swift index ef45504a..c29ae96e 100644 --- a/Mastodon/Scene/Settings/View/AppearanceView.swift +++ b/Mastodon/Scene/Settings/View/AppearanceView.swift @@ -10,15 +10,18 @@ import MastodonAsset import MastodonLocalization class AppearanceView: UIView { + lazy var imageView: UIImageView = { let view = UIImageView() + view.contentMode = .scaleAspectFill view.layer.masksToBounds = true - view.layer.cornerRadius = 14 + view.layer.cornerRadius = 8 view.layer.cornerCurve = .continuous // accessibility view.accessibilityIgnoresInvertColors = true return view }() + lazy var titleLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 12, weight: .regular) @@ -26,35 +29,19 @@ class AppearanceView: UIView { label.textAlignment = .center return label }() - lazy var checkBox: UIButton = { - let button = UIButton() - button.isUserInteractionEnabled = false - button.setImage(UIImage(systemName: "circle"), for: .normal) - button.setImage(UIImage(systemName: "checkmark.circle.fill"), for: .selected) - button.imageView?.preferredSymbolConfiguration = UIImage.SymbolConfiguration(textStyle: .body) - button.imageView?.tintColor = Asset.Colors.Label.secondary.color - button.imageView?.contentMode = .scaleAspectFill - return button - }() + lazy var stackView: UIStackView = { let view = UIStackView() view.axis = .vertical - view.spacing = 10 + view.spacing = 8 view.distribution = .equalSpacing return view }() var selected: Bool = false { - didSet { - checkBox.isSelected = selected - if selected { - checkBox.imageView?.tintColor = Asset.Colors.brandBlue.color - } else { - checkBox.imageView?.tintColor = Asset.Colors.Label.secondary.color - } - } + didSet { setNeedsLayout() } } - + // MARK: - Methods init(image: UIImage?, title: String) { super.init(frame: .zero) @@ -70,23 +57,21 @@ class AppearanceView: UIView { } override var accessibilityLabel: String? { - get { - return [titleLabel.text, checkBox.accessibilityLabel] - .compactMap { $0 } - .joined(separator: ", ") - } + get { titleLabel.text } set { } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + +} + +extension AppearanceView { - // MARK: - Private methods private func setupUI() { stackView.addArrangedSubview(imageView) stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(checkBox) addSubview(stackView) translatesAutoresizingMaskIntoConstraints = false @@ -96,10 +81,37 @@ class AppearanceView: UIView { stackView.leadingAnchor.constraint(equalTo: self.leadingAnchor), stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor), stackView.trailingAnchor.constraint(equalTo: self.trailingAnchor), - imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 218.0 / 100.0), + imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 120.0 / 90.0), ]) } + + private func configureForSelection() { + if selected { + imageView.layer.borderWidth = 3 + imageView.layer.borderColor = Asset.Colors.Label.primary.color.cgColor + accessibilityTraits.insert(.selected) + } else { + imageView.layer.borderWidth = 1 + imageView.layer.borderColor = Asset.Colors.Label.primaryReverse.color.cgColor + accessibilityTraits.remove(.selected) + } + } + + override func layoutSubviews() { + super.layoutSubviews() + + configureForSelection() + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + setNeedsLayout() + } + +} +extension AppearanceView { override func touchesBegan(_ touches: Set, with event: UIEvent?) { super.touchesBegan(touches, with: event) self.alpha = 0.5 diff --git a/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift b/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift deleted file mode 100644 index 1e0754d8..00000000 --- a/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift +++ /dev/null @@ -1,173 +0,0 @@ -// -// SettingsAppearanceTableViewCell.swift -// Mastodon -// -// Created by ihugo on 2021/4/8. -// - -import UIKit -import Combine -import MastodonAsset -import MastodonLocalization - -protocol SettingsAppearanceTableViewCellDelegate: AnyObject { - func settingsAppearanceCell(_ cell: SettingsAppearanceTableViewCell, didSelectAppearanceMode appearanceMode: SettingsItem.AppearanceMode) -} - -class SettingsAppearanceTableViewCell: UITableViewCell { - - var disposeBag = Set() - var observations = Set() - - static let spacing: CGFloat = 18 - - weak var delegate: SettingsAppearanceTableViewCellDelegate? - var appearance: SettingsItem.AppearanceMode = .automatic - - lazy var stackView: UIStackView = { - let view = UIStackView() - view.axis = .horizontal - view.distribution = .fillEqually - view.spacing = SettingsAppearanceTableViewCell.spacing - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - let automatic = AppearanceView(image: Asset.Settings.darkAuto.image, - title: L10n.Scene.Settings.Section.Appearance.automatic) - let light = AppearanceView(image: Asset.Settings.light.image, - title: L10n.Scene.Settings.Section.Appearance.light) - let dark = AppearanceView(image: Asset.Settings.dark.image, - title: L10n.Scene.Settings.Section.Appearance.dark) - - lazy var automaticTap: UITapGestureRecognizer = { - let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer - tapGestureRecognizer.addTarget(self, action: #selector(appearanceDidTap(sender:))) - return tapGestureRecognizer - }() - - lazy var lightTap: UITapGestureRecognizer = { - let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer - tapGestureRecognizer.addTarget(self, action: #selector(appearanceDidTap(sender:))) - return tapGestureRecognizer - }() - - lazy var darkTap: UITapGestureRecognizer = { - let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer - tapGestureRecognizer.addTarget(self, action: #selector(appearanceDidTap(sender:))) - return tapGestureRecognizer - }() - - override func prepareForReuse() { - super.prepareForReuse() - - disposeBag.removeAll() - observations.removeAll() - } - - // MARK: - Methods - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - setupUI() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - - // remove separator line in section of group tableview - for subview in self.subviews { - if subview != self.contentView && subview.frame.width == self.frame.width { - subview.removeFromSuperview() - } - } - - setupAsset(theme: ThemeService.shared.currentTheme.value) - } - - func update(with data: SettingsItem.AppearanceMode) { - appearance = data - - automatic.selected = false - light.selected = false - dark.selected = false - - switch data { - case .automatic: - automatic.selected = true - case .light: - light.selected = true - case .dark: - dark.selected = true - } - } - - // MARK: Private methods - private func setupUI() { - backgroundColor = .clear - selectionStyle = .none - contentView.addSubview(stackView) - - stackView.addArrangedSubview(automatic) - stackView.addArrangedSubview(light) - stackView.addArrangedSubview(dark) - - automatic.addGestureRecognizer(automaticTap) - light.addGestureRecognizer(lightTap) - dark.addGestureRecognizer(darkTap) - - NSLayoutConstraint.activate([ - stackView.topAnchor.constraint(equalTo: contentView.topAnchor), - stackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - stackView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - ]) - - setupAsset(theme: ThemeService.shared.currentTheme.value) - ThemeService.shared.currentTheme - .receive(on: DispatchQueue.main) - .sink { [weak self] theme in - guard let self = self else { return } - self.setupAsset(theme: theme) - } - .store(in: &disposeBag) - } - - private func setupAsset(theme: Theme) { - let aspectRatio = Asset.Settings.light.image.size - let width = floor(frame.width - 2 * SettingsAppearanceTableViewCell.spacing) / 3 - let height = width / aspectRatio.width * aspectRatio.height - let size = CGSize(width: width, height: height) - - light.imageView.image = Asset.Settings.light.image.af.imageAspectScaled(toFill: size, scale: UIScreen.main.scale) - switch theme.themeName { - case .mastodon: - automatic.imageView.image = Asset.Settings.darkAuto.image.af.imageAspectScaled(toFill: size, scale: UIScreen.main.scale) - dark.imageView.image = Asset.Settings.dark.image.af.imageAspectScaled(toFill: size, scale: UIScreen.main.scale) - case .system: - automatic.imageView.image = Asset.Settings.blackAuto.image.af.imageAspectScaled(toFill: size, scale: UIScreen.main.scale) - dark.imageView.image = Asset.Settings.black.image.af.imageAspectScaled(toFill: size, scale: UIScreen.main.scale) - } - } - - // MARK: - Actions - @objc func appearanceDidTap(sender: UIGestureRecognizer) { - if sender == automaticTap { - appearance = .automatic - } - - if sender == lightTap { - appearance = .light - } - - if sender == darkTap { - appearance = .dark - } - - guard let delegate = self.delegate else { return } - delegate.settingsAppearanceCell(self, didSelectAppearanceMode: appearance) - } -}