feat: update setting scene UI

This commit is contained in:
CMK 2022-02-08 18:17:17 +08:00
parent f4bb2d947f
commit 9051e5d1ec
12 changed files with 379 additions and 296 deletions

View File

@ -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",

View File

@ -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 = "<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>"; };
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>"; };
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>"; };
@ -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 */,

View File

@ -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<Setting>)
case preference(settingRecord: ManagedObjectRecord<Setting>, preferenceType: PreferenceType)
case notification(settingRecord: ManagedObjectRecord<Setting>, 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
}

View File

@ -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:

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
}()

View File

@ -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:

View File

@ -108,24 +108,24 @@ extension SettingsViewModel {
var snapshot = NSDiffableDataSourceSnapshot<SettingsSection, SettingsItem>()
// 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] = [

View File

@ -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<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
self.alpha = 0.5

View File

@ -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)
}
}