feat: update setting scene UI
This commit is contained in:
parent
f4bb2d947f
commit
9051e5d1ec
|
@ -514,6 +514,13 @@
|
||||||
"light": "Always Light",
|
"light": "Always Light",
|
||||||
"dark": "Always Dark"
|
"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": {
|
"notifications": {
|
||||||
"title": "Notifications",
|
"title": "Notifications",
|
||||||
"favorites": "Favorites my post",
|
"favorites": "Favorites my post",
|
||||||
|
|
|
@ -446,6 +446,7 @@
|
||||||
DB98EB6527B216500082E365 /* ReportResultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6427B216500082E365 /* ReportResultViewModel.swift */; };
|
DB98EB6527B216500082E365 /* ReportResultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6427B216500082E365 /* ReportResultViewModel.swift */; };
|
||||||
DB98EB6727B216560082E365 /* ReportResultViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6627B216560082E365 /* ReportResultViewModel+Diffable.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 */; };
|
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 */; };
|
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */; };
|
||||||
DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */ = {isa = PBXBuildFile; productRef = DB9A487D2603456B008B817C /* UITextView+Placeholder */; };
|
DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */ = {isa = PBXBuildFile; productRef = DB9A487D2603456B008B817C /* UITextView+Placeholder */; };
|
||||||
DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */; };
|
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 = "<group>"; };
|
DB98EB6427B216500082E365 /* ReportResultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultViewModel.swift; sourceTree = "<group>"; };
|
||||||
DB98EB6627B216560082E365 /* ReportResultViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportResultViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
DB98EB6627B216560082E365 /* ReportResultViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportResultViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||||
DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultActionTableViewCell.swift; sourceTree = "<group>"; };
|
DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultActionTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
DB98EB6A27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsAppearanceTableViewCell+ViewModel.swift"; sourceTree = "<group>"; };
|
||||||
DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentContainerView+EmptyStateView.swift"; sourceTree = "<group>"; };
|
DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentContainerView+EmptyStateView.swift"; sourceTree = "<group>"; };
|
||||||
DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+PublishState.swift"; sourceTree = "<group>"; };
|
DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+PublishState.swift"; sourceTree = "<group>"; };
|
||||||
DB9A488F26035963008B817C /* APIService+Media.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Media.swift"; sourceTree = "<group>"; };
|
DB9A488F26035963008B817C /* APIService+Media.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Media.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -1831,6 +1833,7 @@
|
||||||
5B90C455262599800002E742 /* Settings */ = {
|
5B90C455262599800002E742 /* Settings */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
5B90C458262599800002E742 /* Cell */,
|
||||||
5B90C457262599800002E742 /* View */,
|
5B90C457262599800002E742 /* View */,
|
||||||
DB6D9F9626367249008423CD /* SettingsViewController.swift */,
|
DB6D9F9626367249008423CD /* SettingsViewController.swift */,
|
||||||
5B90C456262599800002E742 /* SettingsViewModel.swift */,
|
5B90C456262599800002E742 /* SettingsViewModel.swift */,
|
||||||
|
@ -1841,7 +1844,6 @@
|
||||||
5B90C457262599800002E742 /* View */ = {
|
5B90C457262599800002E742 /* View */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5B90C458262599800002E742 /* Cell */,
|
|
||||||
5B90C45C262599800002E742 /* SettingsSectionHeader.swift */,
|
5B90C45C262599800002E742 /* SettingsSectionHeader.swift */,
|
||||||
DB443CD32694627B00159B29 /* AppearanceView.swift */,
|
DB443CD32694627B00159B29 /* AppearanceView.swift */,
|
||||||
);
|
);
|
||||||
|
@ -1851,8 +1853,9 @@
|
||||||
5B90C458262599800002E742 /* Cell */ = {
|
5B90C458262599800002E742 /* Cell */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */,
|
|
||||||
5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */,
|
5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */,
|
||||||
|
DB98EB6A27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift */,
|
||||||
|
5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */,
|
||||||
5B90C45B262599800002E742 /* SettingsLinkTableViewCell.swift */,
|
5B90C45B262599800002E742 /* SettingsLinkTableViewCell.swift */,
|
||||||
);
|
);
|
||||||
path = Cell;
|
path = Cell;
|
||||||
|
@ -4196,6 +4199,7 @@
|
||||||
DB6B7500272FF73800C70B6E /* UserTableViewCell.swift in Sources */,
|
DB6B7500272FF73800C70B6E /* UserTableViewCell.swift in Sources */,
|
||||||
DB1D842E26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift in Sources */,
|
DB1D842E26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift in Sources */,
|
||||||
DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */,
|
DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */,
|
||||||
|
DB98EB6B27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift in Sources */,
|
||||||
DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */,
|
DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */,
|
||||||
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
||||||
DB4932B926F31AD300EF46D4 /* BadgeButton.swift in Sources */,
|
DB4932B926F31AD300EF46D4 /* BadgeButton.swift in Sources */,
|
||||||
|
|
|
@ -7,13 +7,14 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import CoreData
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
import MastodonAsset
|
import MastodonAsset
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
|
|
||||||
enum SettingsItem {
|
enum SettingsItem {
|
||||||
case appearance(settingObjectID: NSManagedObjectID)
|
case appearance(record: ManagedObjectRecord<Setting>)
|
||||||
case notification(settingObjectID: NSManagedObjectID, switchMode: NotificationSwitchMode)
|
case preference(settingRecord: ManagedObjectRecord<Setting>, preferenceType: PreferenceType)
|
||||||
case preference(settingObjectID: NSManagedObjectID, preferenceType: PreferenceType)
|
case notification(settingRecord: ManagedObjectRecord<Setting>, switchMode: NotificationSwitchMode)
|
||||||
case boringZone(item: Link)
|
case boringZone(item: Link)
|
||||||
case spicyZone(item: Link)
|
case spicyZone(item: Link)
|
||||||
}
|
}
|
||||||
|
@ -21,9 +22,10 @@ enum SettingsItem {
|
||||||
extension SettingsItem {
|
extension SettingsItem {
|
||||||
|
|
||||||
enum AppearanceMode: String {
|
enum AppearanceMode: String {
|
||||||
case automatic
|
case system
|
||||||
|
case reallyDark
|
||||||
|
case sortaDark
|
||||||
case light
|
case light
|
||||||
case dark
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum NotificationSwitchMode: CaseIterable, Hashable {
|
enum NotificationSwitchMode: CaseIterable, Hashable {
|
||||||
|
@ -43,14 +45,12 @@ extension SettingsItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PreferenceType: CaseIterable {
|
enum PreferenceType: CaseIterable {
|
||||||
case darkMode
|
|
||||||
case disableAvatarAnimation
|
case disableAvatarAnimation
|
||||||
case disableEmojiAnimation
|
case disableEmojiAnimation
|
||||||
case useDefaultBrowser
|
case useDefaultBrowser
|
||||||
|
|
||||||
var title: String {
|
var title: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .darkMode: return L10n.Scene.Settings.Section.Preference.trueBlackDarkMode
|
|
||||||
case .disableAvatarAnimation: return L10n.Scene.Settings.Section.Preference.disableAvatarAnimation
|
case .disableAvatarAnimation: return L10n.Scene.Settings.Section.Preference.disableAvatarAnimation
|
||||||
case .disableEmojiAnimation: return L10n.Scene.Settings.Section.Preference.disableEmojiAnimation
|
case .disableEmojiAnimation: return L10n.Scene.Settings.Section.Preference.disableEmojiAnimation
|
||||||
case .useDefaultBrowser: return L10n.Scene.Settings.Section.Preference.usingDefaultBrowser
|
case .useDefaultBrowser: return L10n.Scene.Settings.Section.Preference.usingDefaultBrowser
|
||||||
|
@ -77,12 +77,12 @@ extension SettingsItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var textColor: UIColor {
|
var textColor: UIColor? {
|
||||||
switch self {
|
switch self {
|
||||||
case .accountSettings: return Asset.Colors.brandBlue.color
|
case .accountSettings: return nil // tintColor
|
||||||
case .github: return Asset.Colors.brandBlue.color
|
case .github: return nil
|
||||||
case .termsOfService: return Asset.Colors.brandBlue.color
|
case .termsOfService: return nil
|
||||||
case .privacyPolicy: return Asset.Colors.brandBlue.color
|
case .privacyPolicy: return nil
|
||||||
case .clearMediaCache: return .systemRed
|
case .clearMediaCache: return .systemRed
|
||||||
case .signOut: return .systemRed
|
case .signOut: return .systemRed
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,16 +13,16 @@ import MastodonLocalization
|
||||||
|
|
||||||
enum SettingsSection: Hashable {
|
enum SettingsSection: Hashable {
|
||||||
case appearance
|
case appearance
|
||||||
case notifications
|
|
||||||
case preference
|
case preference
|
||||||
|
case notifications
|
||||||
case boringZone
|
case boringZone
|
||||||
case spicyZone
|
case spicyZone
|
||||||
|
|
||||||
var title: String {
|
var title: String {
|
||||||
switch self {
|
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 .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 .boringZone: return L10n.Scene.Settings.Section.BoringZone.title
|
||||||
case .spicyZone: return L10n.Scene.Settings.Section.SpicyZone.title
|
case .spicyZone: return L10n.Scene.Settings.Section.SpicyZone.title
|
||||||
}
|
}
|
||||||
|
@ -41,25 +41,38 @@ extension SettingsSection {
|
||||||
weak settingsToggleCellDelegate
|
weak settingsToggleCellDelegate
|
||||||
] tableView, indexPath, item -> UITableViewCell? in
|
] tableView, indexPath, item -> UITableViewCell? in
|
||||||
switch item {
|
switch item {
|
||||||
case .appearance(let objectID):
|
case .appearance(let record):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsAppearanceTableViewCell.self), for: indexPath) as! SettingsAppearanceTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsAppearanceTableViewCell.self), for: indexPath) as! SettingsAppearanceTableViewCell
|
||||||
UserDefaults.shared.observe(\.customUserInterfaceStyle, options: [.initial, .new]) { [weak cell] defaults, _ in
|
managedObjectContext.performAndWait {
|
||||||
guard let cell = cell else { return }
|
guard let setting = record.object(in: managedObjectContext) else { return }
|
||||||
switch defaults.customUserInterfaceStyle {
|
cell.configure(setting: setting)
|
||||||
case .unspecified: cell.update(with: .automatic)
|
|
||||||
case .dark: cell.update(with: .dark)
|
|
||||||
case .light: cell.update(with: .light)
|
|
||||||
@unknown default:
|
|
||||||
assertionFailure()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.store(in: &cell.observations)
|
|
||||||
cell.delegate = settingsAppearanceTableViewCellDelegate
|
cell.delegate = settingsAppearanceTableViewCellDelegate
|
||||||
return cell
|
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
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsToggleTableViewCell.self), for: indexPath) as! SettingsToggleTableViewCell
|
||||||
managedObjectContext.performAndWait {
|
managedObjectContext.performAndWait {
|
||||||
let setting = managedObjectContext.object(with: objectID) as! Setting
|
guard let setting = record.object(in: managedObjectContext) else { return }
|
||||||
if let subscription = setting.activeSubscription {
|
if let subscription = setting.activeSubscription {
|
||||||
SettingsSection.configureSettingToggle(cell: cell, switchMode: switchMode, subscription: subscription)
|
SettingsSection.configureSettingToggle(cell: cell, switchMode: switchMode, subscription: subscription)
|
||||||
}
|
}
|
||||||
|
@ -77,32 +90,12 @@ extension SettingsSection {
|
||||||
}
|
}
|
||||||
cell.delegate = settingsToggleCellDelegate
|
cell.delegate = settingsToggleCellDelegate
|
||||||
return cell
|
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),
|
case .boringZone(let item),
|
||||||
.spicyZone(let item):
|
.spicyZone(let item):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsLinkTableViewCell.self), for: indexPath) as! SettingsLinkTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsLinkTableViewCell.self), for: indexPath) as! SettingsLinkTableViewCell
|
||||||
cell.update(with: item)
|
cell.update(with: item)
|
||||||
return cell
|
return cell
|
||||||
}
|
} // end switch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,8 +112,6 @@ extension SettingsSection {
|
||||||
cell.textLabel?.text = preferenceType.title
|
cell.textLabel?.text = preferenceType.title
|
||||||
|
|
||||||
switch preferenceType {
|
switch preferenceType {
|
||||||
case .darkMode:
|
|
||||||
cell.switchButton.isOn = setting.preferredTrueBlackDarkMode
|
|
||||||
case .disableAvatarAnimation:
|
case .disableAvatarAnimation:
|
||||||
cell.switchButton.isOn = setting.preferredStaticAvatar
|
cell.switchButton.isOn = setting.preferredStaticAvatar
|
||||||
case .disableEmojiAnimation:
|
case .disableEmojiAnimation:
|
||||||
|
|
|
@ -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<AnyCancellable>()
|
||||||
|
private var observations = Set<NSKeyValueObservation>()
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<AnyCancellable>()
|
||||||
|
var observations = Set<NSKeyValueObservation>()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ class SettingsToggleTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
private(set) lazy var switchButton: UISwitch = {
|
private(set) lazy var switchButton: UISwitch = {
|
||||||
let view = UISwitch(frame:.zero)
|
let view = UISwitch(frame:.zero)
|
||||||
|
view.onTintColor = contentView.window?.tintColor ?? .label
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -99,15 +99,13 @@ class SettingsViewController: UIViewController, NeedsDependency {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private(set) lazy var tableView: UITableView = {
|
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 = {
|
let style: UITableView.Style = {
|
||||||
switch UIDevice.current.userInterfaceIdiom {
|
switch UIDevice.current.userInterfaceIdiom {
|
||||||
case .phone:
|
case .phone: return .grouped
|
||||||
return .grouped
|
default: return .insetGrouped
|
||||||
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)
|
let tableView = UITableView(frame: CGRect(x: 0, y: 0, width: 320, height: 320), style: style)
|
||||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
tableView.delegate = self
|
tableView.delegate = self
|
||||||
|
@ -135,6 +133,15 @@ class SettingsViewController: UIViewController, NeedsDependency {
|
||||||
return view
|
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() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
@ -214,7 +221,7 @@ class SettingsViewController: UIViewController, NeedsDependency {
|
||||||
private func setupView() {
|
private func setupView() {
|
||||||
setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
|
setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
|
||||||
ThemeService.shared.currentTheme
|
ThemeService.shared.currentTheme
|
||||||
.receive(on: RunLoop.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] theme in
|
.sink { [weak self] theme in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.setupBackgroundColor(theme: theme)
|
self.setupBackgroundColor(theme: theme)
|
||||||
|
@ -314,10 +321,6 @@ class SettingsViewController: UIViewController, NeedsDependency {
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark: - Actions
|
// Mark: - Actions
|
||||||
|
@ -327,7 +330,9 @@ extension SettingsViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableViewDelegate
|
||||||
extension SettingsViewController: UITableViewDelegate {
|
extension SettingsViewController: UITableViewDelegate {
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||||
let sections = viewModel.dataSource.snapshot().sectionIdentifiers
|
let sections = viewModel.dataSource.snapshot().sectionIdentifiers
|
||||||
guard section < sections.count else { return nil }
|
guard section < sections.count else { return nil }
|
||||||
|
@ -449,24 +454,42 @@ extension SettingsViewController {
|
||||||
|
|
||||||
// MARK: - SettingsAppearanceTableViewCellDelegate
|
// MARK: - SettingsAppearanceTableViewCellDelegate
|
||||||
extension SettingsViewController: 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 dataSource = viewModel.dataSource else { return }
|
||||||
guard let indexPath = tableView.indexPath(for: cell) else { return }
|
guard let indexPath = tableView.indexPath(for: cell) else { return }
|
||||||
let item = dataSource.itemIdentifier(for: indexPath)
|
let item = dataSource.itemIdentifier(for: indexPath)
|
||||||
guard case .appearance = item else { return }
|
guard case let .appearance(record) = item else { return }
|
||||||
|
|
||||||
switch appearanceMode {
|
|
||||||
case .automatic:
|
|
||||||
UserDefaults.shared.customUserInterfaceStyle = .unspecified
|
|
||||||
case .light:
|
|
||||||
UserDefaults.shared.customUserInterfaceStyle = .light
|
|
||||||
case .dark:
|
|
||||||
UserDefaults.shared.customUserInterfaceStyle = .dark
|
|
||||||
}
|
|
||||||
|
|
||||||
let feedbackGenerator = UIImpactFeedbackGenerator(style: .light)
|
Task { @MainActor in
|
||||||
feedbackGenerator.impactOccurred()
|
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 {
|
extension SettingsViewController: SettingsToggleCellDelegate {
|
||||||
|
@ -478,10 +501,10 @@ extension SettingsViewController: SettingsToggleCellDelegate {
|
||||||
let item = dataSource.itemIdentifier(for: indexPath)
|
let item = dataSource.itemIdentifier(for: indexPath)
|
||||||
|
|
||||||
switch item {
|
switch item {
|
||||||
case .notification(let settingObjectID, let switchMode):
|
case .notification(let record, let switchMode):
|
||||||
let managedObjectContext = context.backgroundManagedObjectContext
|
let managedObjectContext = context.backgroundManagedObjectContext
|
||||||
managedObjectContext.performChanges {
|
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 }
|
guard let subscription = setting.activeSubscription else { return }
|
||||||
let alert = subscription.alert
|
let alert = subscription.alert
|
||||||
switch switchMode {
|
switch switchMode {
|
||||||
|
@ -497,13 +520,11 @@ extension SettingsViewController: SettingsToggleCellDelegate {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
case .preference(let settingObjectID, let preferenceType):
|
case .preference(let record, let preferenceType):
|
||||||
let managedObjectContext = context.backgroundManagedObjectContext
|
let managedObjectContext = context.backgroundManagedObjectContext
|
||||||
managedObjectContext.performChanges {
|
managedObjectContext.performChanges {
|
||||||
let setting = managedObjectContext.object(with: settingObjectID) as! Setting
|
guard let setting = record.object(in: managedObjectContext) else { return }
|
||||||
switch preferenceType {
|
switch preferenceType {
|
||||||
case .darkMode:
|
|
||||||
setting.update(preferredTrueBlackDarkMode: isOn)
|
|
||||||
case .disableAvatarAnimation:
|
case .disableAvatarAnimation:
|
||||||
setting.update(preferredStaticAvatar: isOn)
|
setting.update(preferredStaticAvatar: isOn)
|
||||||
case .disableEmojiAnimation:
|
case .disableEmojiAnimation:
|
||||||
|
@ -516,8 +537,6 @@ extension SettingsViewController: SettingsToggleCellDelegate {
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
switch preferenceType {
|
switch preferenceType {
|
||||||
case .darkMode:
|
|
||||||
ThemeService.shared.set(themeName: isOn ? .system : .mastodon)
|
|
||||||
case .disableAvatarAnimation:
|
case .disableAvatarAnimation:
|
||||||
UserDefaults.shared.preferredStaticAvatar = isOn
|
UserDefaults.shared.preferredStaticAvatar = isOn
|
||||||
case .disableEmojiAnimation:
|
case .disableEmojiAnimation:
|
||||||
|
|
|
@ -108,24 +108,24 @@ extension SettingsViewModel {
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<SettingsSection, SettingsItem>()
|
var snapshot = NSDiffableDataSourceSnapshot<SettingsSection, SettingsItem>()
|
||||||
|
|
||||||
// appearance
|
// appearance
|
||||||
let appearanceItems = [SettingsItem.appearance(settingObjectID: setting.objectID)]
|
let appearanceItems = [SettingsItem.appearance(record: .init(objectID: setting.objectID))]
|
||||||
snapshot.appendSections([.appearance])
|
snapshot.appendSections([.appearance])
|
||||||
snapshot.appendItems(appearanceItems, toSection: .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
|
// preference
|
||||||
snapshot.appendSections([.preference])
|
snapshot.appendSections([.preference])
|
||||||
let preferenceItems: [SettingsItem] = SettingsItem.PreferenceType.allCases.map { preferenceType in
|
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)
|
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
|
// boring zone
|
||||||
let boringZoneSettingsItems: [SettingsItem] = {
|
let boringZoneSettingsItems: [SettingsItem] = {
|
||||||
let links: [SettingsItem.Link] = [
|
let links: [SettingsItem.Link] = [
|
||||||
|
|
|
@ -10,15 +10,18 @@ import MastodonAsset
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
|
|
||||||
class AppearanceView: UIView {
|
class AppearanceView: UIView {
|
||||||
|
|
||||||
lazy var imageView: UIImageView = {
|
lazy var imageView: UIImageView = {
|
||||||
let view = UIImageView()
|
let view = UIImageView()
|
||||||
|
view.contentMode = .scaleAspectFill
|
||||||
view.layer.masksToBounds = true
|
view.layer.masksToBounds = true
|
||||||
view.layer.cornerRadius = 14
|
view.layer.cornerRadius = 8
|
||||||
view.layer.cornerCurve = .continuous
|
view.layer.cornerCurve = .continuous
|
||||||
// accessibility
|
// accessibility
|
||||||
view.accessibilityIgnoresInvertColors = true
|
view.accessibilityIgnoresInvertColors = true
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var titleLabel: UILabel = {
|
lazy var titleLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.font = .systemFont(ofSize: 12, weight: .regular)
|
label.font = .systemFont(ofSize: 12, weight: .regular)
|
||||||
|
@ -26,35 +29,19 @@ class AppearanceView: UIView {
|
||||||
label.textAlignment = .center
|
label.textAlignment = .center
|
||||||
return label
|
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 = {
|
lazy var stackView: UIStackView = {
|
||||||
let view = UIStackView()
|
let view = UIStackView()
|
||||||
view.axis = .vertical
|
view.axis = .vertical
|
||||||
view.spacing = 10
|
view.spacing = 8
|
||||||
view.distribution = .equalSpacing
|
view.distribution = .equalSpacing
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var selected: Bool = false {
|
var selected: Bool = false {
|
||||||
didSet {
|
didSet { setNeedsLayout() }
|
||||||
checkBox.isSelected = selected
|
|
||||||
if selected {
|
|
||||||
checkBox.imageView?.tintColor = Asset.Colors.brandBlue.color
|
|
||||||
} else {
|
|
||||||
checkBox.imageView?.tintColor = Asset.Colors.Label.secondary.color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Methods
|
// MARK: - Methods
|
||||||
init(image: UIImage?, title: String) {
|
init(image: UIImage?, title: String) {
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
@ -70,23 +57,21 @@ class AppearanceView: UIView {
|
||||||
|
|
||||||
}
|
}
|
||||||
override var accessibilityLabel: String? {
|
override var accessibilityLabel: String? {
|
||||||
get {
|
get { titleLabel.text }
|
||||||
return [titleLabel.text, checkBox.accessibilityLabel]
|
|
||||||
.compactMap { $0 }
|
|
||||||
.joined(separator: ", ")
|
|
||||||
}
|
|
||||||
set { }
|
set { }
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppearanceView {
|
||||||
|
|
||||||
// MARK: - Private methods
|
|
||||||
private func setupUI() {
|
private func setupUI() {
|
||||||
stackView.addArrangedSubview(imageView)
|
stackView.addArrangedSubview(imageView)
|
||||||
stackView.addArrangedSubview(titleLabel)
|
stackView.addArrangedSubview(titleLabel)
|
||||||
stackView.addArrangedSubview(checkBox)
|
|
||||||
|
|
||||||
addSubview(stackView)
|
addSubview(stackView)
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -96,10 +81,37 @@ class AppearanceView: UIView {
|
||||||
stackView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
stackView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
||||||
stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
||||||
stackView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
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<UITouch>, with event: UIEvent?) {
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
super.touchesBegan(touches, with: event)
|
super.touchesBegan(touches, with: event)
|
||||||
self.alpha = 0.5
|
self.alpha = 0.5
|
||||||
|
|
|
@ -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<AnyCancellable>()
|
|
||||||
var observations = Set<NSKeyValueObservation>()
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue