forked from zelo72/mastodon-ios
feat: update setting scene UI
This commit is contained in:
parent
f4bb2d947f
commit
9051e5d1ec
|
@ -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",
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 = {
|
||||
let view = UISwitch(frame:.zero)
|
||||
view.onTintColor = contentView.window?.tintColor ?? .label
|
||||
return view
|
||||
}()
|
||||
|
|
@ -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:
|
||||
|
|
|
@ -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] = [
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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