2021-04-08 13:47:31 +02:00
//
// S e t t i n g s V i e w C o n t r o l l e r . s w i f t
// M a s t o d o n
//
// C r e a t e d b y i h u g o o n 2 0 2 1 / 4 / 7 .
//
import os . log
import UIKit
import Combine
import ActiveLabel
import CoreData
import CoreDataStack
2021-04-26 10:57:50 +02:00
import MastodonSDK
2021-06-01 08:07:44 +02:00
2021-04-08 13:47:31 +02:00
class SettingsViewController : UIViewController , NeedsDependency {
weak var context : AppContext ! { willSet { precondition ( ! isViewLoaded ) } }
weak var coordinator : SceneCoordinator ! { willSet { precondition ( ! isViewLoaded ) } }
var viewModel : SettingsViewModel ! { willSet { precondition ( ! isViewLoaded ) } }
var disposeBag = Set < AnyCancellable > ( )
2021-04-26 10:57:50 +02:00
var notificationPolicySubscription : AnyCancellable ?
2021-04-08 13:47:31 +02:00
var triggerMenu : UIMenu {
let anyone = L10n . Scene . Settings . Section . Notifications . Trigger . anyone
let follower = L10n . Scene . Settings . Section . Notifications . Trigger . follower
let follow = L10n . Scene . Settings . Section . Notifications . Trigger . follow
2021-04-13 10:22:41 +02:00
let noOne = L10n . Scene . Settings . Section . Notifications . Trigger . noone
2021-04-08 13:47:31 +02:00
let menu = UIMenu (
2021-04-12 15:42:43 +02:00
image : nil ,
2021-04-08 13:47:31 +02:00
identifier : nil ,
options : . displayInline ,
children : [
UIAction ( title : anyone , image : UIImage ( systemName : " person.3 " ) , attributes : [ ] ) { [ weak self ] action in
2021-04-26 10:57:50 +02:00
self ? . updateTrigger ( policy : . all )
2021-04-08 13:47:31 +02:00
} ,
UIAction ( title : follower , image : UIImage ( systemName : " person.crop.circle.badge.plus " ) , attributes : [ ] ) { [ weak self ] action in
2021-04-26 10:57:50 +02:00
self ? . updateTrigger ( policy : . follower )
2021-04-08 13:47:31 +02:00
} ,
UIAction ( title : follow , image : UIImage ( systemName : " person.crop.circle.badge.checkmark " ) , attributes : [ ] ) { [ weak self ] action in
2021-04-26 10:57:50 +02:00
self ? . updateTrigger ( policy : . followed )
2021-04-08 13:47:31 +02:00
} ,
UIAction ( title : noOne , image : UIImage ( systemName : " nosign " ) , attributes : [ ] ) { [ weak self ] action in
2021-04-26 10:57:50 +02:00
self ? . updateTrigger ( policy : . none )
2021-04-08 13:47:31 +02:00
} ,
2021-04-17 20:02:08 +02:00
]
2021-04-08 13:47:31 +02:00
)
return menu
}
2021-05-10 12:48:04 +02:00
private let notifySectionHeaderStackView : UIStackView = {
2021-04-08 13:47:31 +02:00
let view = UIStackView ( )
view . translatesAutoresizingMaskIntoConstraints = false
view . isLayoutMarginsRelativeArrangement = true
view . axis = . horizontal
view . spacing = 4
2021-05-10 12:48:04 +02:00
return view
} ( )
2021-05-11 11:07:08 +02:00
let notifyLabel = UILabel ( )
2021-05-10 12:48:04 +02:00
private ( set ) lazy var notifySectionHeader : UIView = {
let view = notifySectionHeaderStackView
2021-04-08 13:47:31 +02:00
notifyLabel . translatesAutoresizingMaskIntoConstraints = false
2021-05-11 11:07:08 +02:00
notifyLabel . adjustsFontForContentSizeCategory = true
2021-05-10 12:48:04 +02:00
notifyLabel . font = UIFontMetrics ( forTextStyle : . headline ) . scaledFont ( for : UIFont . systemFont ( ofSize : 20 , weight : . semibold ) )
2021-04-08 13:47:31 +02:00
notifyLabel . textColor = Asset . Colors . Label . primary . color
notifyLabel . text = L10n . Scene . Settings . Section . Notifications . Trigger . title
view . addArrangedSubview ( notifyLabel )
view . addArrangedSubview ( whoButton )
2021-05-11 11:07:08 +02:00
whoButton . setContentHuggingPriority ( . defaultHigh + 1 , for : . horizontal )
whoButton . setContentHuggingPriority ( . defaultHigh + 1 , for : . vertical )
2021-04-08 13:47:31 +02:00
return view
} ( )
2021-04-26 10:57:50 +02:00
private ( set ) lazy var whoButton : UIButton = {
2021-04-08 13:47:31 +02:00
let whoButton = UIButton ( type : . roundedRect )
whoButton . menu = triggerMenu
whoButton . showsMenuAsPrimaryAction = true
whoButton . setBackgroundColor ( Asset . Colors . battleshipGrey . color , for : . normal )
whoButton . setTitleColor ( Asset . Colors . Label . primary . color , for : . normal )
2021-05-11 11:07:08 +02:00
whoButton . titleLabel ? . adjustsFontForContentSizeCategory = true
2021-04-08 13:47:31 +02:00
whoButton . titleLabel ? . font = UIFontMetrics ( forTextStyle : . title3 ) . scaledFont ( for : UIFont . systemFont ( ofSize : 20 , weight : . semibold ) )
whoButton . contentEdgeInsets = UIEdgeInsets ( top : 5 , left : 5 , bottom : 5 , right : 5 )
whoButton . layer . cornerRadius = 10
whoButton . clipsToBounds = true
return whoButton
} ( )
2021-04-26 10:57:50 +02:00
private ( set ) lazy var tableView : UITableView = {
2021-04-08 13:47:31 +02:00
// i n i t w i t h a f r a m e t o f i x a c o n f l i c t ( ' U I V i e w - E n c a p s u l a t e d - L a y o u t - W i d t h ' U I S t a c k V i e w : 0 x 7 f 8 c 2 b 6 c 0 5 9 0 . w i d t h = = 0 )
let tableView = UITableView ( frame : CGRect ( x : 0 , y : 0 , width : 320 , height : 320 ) , style : . grouped )
tableView . translatesAutoresizingMaskIntoConstraints = false
tableView . delegate = self
tableView . rowHeight = UITableView . automaticDimension
tableView . backgroundColor = Asset . Colors . Background . systemGroupedBackground . color
2021-04-26 10:57:50 +02:00
tableView . register ( SettingsAppearanceTableViewCell . self , forCellReuseIdentifier : String ( describing : SettingsAppearanceTableViewCell . self ) )
tableView . register ( SettingsToggleTableViewCell . self , forCellReuseIdentifier : String ( describing : SettingsToggleTableViewCell . self ) )
tableView . register ( SettingsLinkTableViewCell . self , forCellReuseIdentifier : String ( describing : SettingsLinkTableViewCell . self ) )
2021-04-08 13:47:31 +02:00
return tableView
} ( )
2021-04-26 10:57:50 +02:00
lazy var tableFooterView : UIView = {
2021-04-08 13:47:31 +02:00
// i n i t w i t h a f r a m e t o f i x a c o n f l i c t ( ' U I V i e w - E n c a p s u l a t e d - L a y o u t - H e i g h t ' U I S t a c k V i e w : 0 x 7 f f e 4 1 e 4 7 d a 0 . h e i g h t = = 0 )
let view = UIStackView ( frame : CGRect ( x : 0 , y : 0 , width : 320 , height : 320 ) )
view . isLayoutMarginsRelativeArrangement = true
view . layoutMargins = UIEdgeInsets ( top : 20 , left : 20 , bottom : 20 , right : 20 )
view . axis = . vertical
view . alignment = . center
let label = ActiveLabel ( style : . default )
2021-05-11 11:07:08 +02:00
label . adjustsFontForContentSizeCategory = true
2021-04-08 13:47:31 +02:00
label . textAlignment = . center
2021-05-07 12:25:57 +02:00
label . configure ( content : " Mastodon is open source software. You can contribute or report issues on GitHub at <a href= \" https://github.com/tootsuite/mastodon \" >tootsuite/mastodon</a> (v3.3.0). " , emojiDict : [ : ] )
2021-04-08 13:47:31 +02:00
label . delegate = self
view . addArrangedSubview ( label )
return view
} ( )
override func viewDidLoad ( ) {
super . viewDidLoad ( )
setupView ( )
bindViewModel ( )
viewModel . viewDidLoad . send ( )
}
override func viewDidLayoutSubviews ( ) {
super . viewDidLayoutSubviews ( )
guard let footerView = self . tableView . tableFooterView else {
return
}
let width = self . tableView . bounds . size . width
let size = footerView . systemLayoutSizeFitting ( CGSize ( width : width , height : UIView . layoutFittingCompressedSize . height ) )
if footerView . frame . size . height != size . height {
footerView . frame . size . height = size . height
self . tableView . tableFooterView = footerView
}
}
2021-05-10 12:48:04 +02:00
override func traitCollectionDidChange ( _ previousTraitCollection : UITraitCollection ? ) {
super . traitCollectionDidChange ( previousTraitCollection )
updateSectionHeaderStackViewLayout ( )
}
2021-04-08 13:47:31 +02:00
// M A K R : - P r i v a t e m e t h o d s
2021-05-10 12:48:04 +02:00
private func updateSectionHeaderStackViewLayout ( ) {
2021-05-11 11:07:08 +02:00
// a c c e s s i b i l i t y
2021-05-10 12:48:04 +02:00
if traitCollection . preferredContentSizeCategory < . accessibilityMedium {
notifySectionHeaderStackView . axis = . horizontal
2021-05-11 11:07:08 +02:00
notifyLabel . numberOfLines = 1
2021-05-10 12:48:04 +02:00
} else {
notifySectionHeaderStackView . axis = . vertical
2021-05-11 11:07:08 +02:00
notifyLabel . numberOfLines = 0
2021-05-10 12:48:04 +02:00
}
}
2021-04-08 13:47:31 +02:00
private func bindViewModel ( ) {
2021-04-26 10:57:50 +02:00
self . whoButton . setTitle ( viewModel . setting . value . activeSubscription ? . policy . title , for : . normal )
viewModel . setting
. sink { [ weak self ] setting in
guard let self = self else { return }
self . notificationPolicySubscription = ManagedObjectObserver . observe ( object : setting )
. sink { _ in
// d o n o t h i n g
} receiveValue : { [ weak self ] change in
guard let self = self else { return }
guard case let . update ( object ) = change . changeType ,
let setting = object as ? Setting else { return }
if let activeSubscription = setting . activeSubscription {
self . whoButton . setTitle ( activeSubscription . policy . title , for : . normal )
} else {
assertionFailure ( )
}
}
}
. store ( in : & disposeBag )
2021-04-08 13:47:31 +02:00
}
private func setupView ( ) {
2021-04-19 14:34:08 +02:00
view . backgroundColor = Asset . Colors . Background . secondarySystemBackground . color
2021-04-08 13:47:31 +02:00
setupNavigation ( )
view . addSubview ( tableView )
NSLayoutConstraint . activate ( [
tableView . topAnchor . constraint ( equalTo : view . topAnchor ) ,
tableView . leadingAnchor . constraint ( equalTo : view . leadingAnchor ) ,
tableView . trailingAnchor . constraint ( equalTo : view . trailingAnchor ) ,
tableView . bottomAnchor . constraint ( equalTo : view . bottomAnchor ) ,
] )
2021-04-26 10:57:50 +02:00
setupTableView ( )
2021-05-10 12:48:04 +02:00
updateSectionHeaderStackViewLayout ( )
2021-04-08 13:47:31 +02:00
}
private func setupNavigation ( ) {
navigationController ? . navigationBar . prefersLargeTitles = true
navigationItem . rightBarButtonItem
= UIBarButtonItem ( barButtonSystemItem : UIBarButtonItem . SystemItem . done ,
target : self ,
action : #selector ( doneButtonDidClick ) )
navigationItem . title = L10n . Scene . Settings . title
let barAppearance = UINavigationBarAppearance ( )
barAppearance . configureWithDefaultBackground ( )
navigationItem . standardAppearance = barAppearance
navigationItem . compactAppearance = barAppearance
navigationItem . scrollEdgeAppearance = barAppearance
}
private func setupTableView ( ) {
2021-04-26 10:57:50 +02:00
viewModel . setupDiffableDataSource (
for : tableView ,
settingsAppearanceTableViewCellDelegate : self ,
settingsToggleCellDelegate : self
)
tableView . tableFooterView = tableFooterView
2021-04-08 13:47:31 +02:00
}
2021-04-13 10:22:41 +02:00
func alertToSignout ( ) {
let alertController = UIAlertController (
title : L10n . Common . Alerts . SignOut . title ,
message : L10n . Common . Alerts . SignOut . message ,
preferredStyle : . alert
)
let cancelAction = UIAlertAction ( title : L10n . Common . Controls . Actions . cancel , style : . cancel , handler : nil )
let signOutAction = UIAlertAction ( title : L10n . Common . Alerts . SignOut . confirm , style : . destructive ) { [ weak self ] _ in
guard let self = self else { return }
2021-04-26 10:57:50 +02:00
self . signOut ( )
2021-04-13 10:22:41 +02:00
}
alertController . addAction ( cancelAction )
alertController . addAction ( signOutAction )
self . coordinator . present (
scene : . alertController ( alertController : alertController ) ,
from : self ,
transition : . alertController ( animated : true , completion : nil )
)
}
2021-04-26 10:57:50 +02:00
func signOut ( ) {
2021-04-08 13:47:31 +02:00
guard let activeMastodonAuthenticationBox = context . authenticationService . activeMastodonAuthenticationBox . value else {
return
}
2021-04-13 10:22:41 +02:00
2021-04-08 13:47:31 +02:00
context . authenticationService . signOutMastodonUser (
domain : activeMastodonAuthenticationBox . domain ,
userID : activeMastodonAuthenticationBox . userID
)
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] result in
guard let self = self else { return }
switch result {
case . failure ( let error ) :
assertionFailure ( error . localizedDescription )
case . success ( let isSignOut ) :
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: sign out %s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function , isSignOut ? " success " : " fail " )
guard isSignOut else { return }
self . coordinator . setup ( )
self . coordinator . setupOnboardingIfNeeds ( animated : true )
}
}
. store ( in : & disposeBag )
}
2021-04-12 15:42:43 +02:00
deinit {
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
}
2021-04-26 10:57:50 +02:00
}
// M a r k : - A c t i o n s
extension SettingsViewController {
@objc private func doneButtonDidClick ( ) {
2021-04-08 13:47:31 +02:00
dismiss ( animated : true , completion : nil )
}
}
extension SettingsViewController : UITableViewDelegate {
func tableView ( _ tableView : UITableView , viewForHeaderInSection section : Int ) -> UIView ? {
let sections = viewModel . dataSource . snapshot ( ) . sectionIdentifiers
guard section < sections . count else { return nil }
2021-04-26 10:57:50 +02:00
let sectionIdentifier = sections [ section ]
2021-04-08 13:47:31 +02:00
2021-04-25 06:48:29 +02:00
let header : SettingsSectionHeader
2021-04-26 10:57:50 +02:00
switch sectionIdentifier {
case . notifications :
2021-04-25 06:48:29 +02:00
header = SettingsSectionHeader (
2021-04-08 13:47:31 +02:00
frame : CGRect ( x : 0 , y : 0 , width : 375 , height : 66 ) ,
customView : notifySectionHeader )
2021-04-26 10:57:50 +02:00
header . update ( title : sectionIdentifier . title )
default :
2021-04-25 06:48:29 +02:00
header = SettingsSectionHeader ( frame : CGRect ( x : 0 , y : 0 , width : 375 , height : 66 ) )
2021-04-26 10:57:50 +02:00
header . update ( title : sectionIdentifier . title )
2021-04-08 13:47:31 +02:00
}
2021-04-25 06:48:29 +02:00
header . preservesSuperviewLayoutMargins = true
2021-04-26 10:57:50 +02:00
2021-04-25 06:48:29 +02:00
return header
2021-04-08 13:47:31 +02:00
}
2021-04-26 10:57:50 +02:00
2021-04-08 13:47:31 +02:00
// r e m o v e t h e g a p o f t a b l e ' s f o o t e r
func tableView ( _ tableView : UITableView , viewForFooterInSection section : Int ) -> UIView ? {
return UIView ( )
}
2021-04-26 10:57:50 +02:00
2021-04-08 13:47:31 +02:00
// r e m o v e t h e g a p o f t a b l e ' s f o o t e r
func tableView ( _ tableView : UITableView , heightForFooterInSection section : Int ) -> CGFloat {
2021-04-26 10:57:50 +02:00
return CGFloat . leastNonzeroMagnitude
2021-04-08 13:47:31 +02:00
}
2021-04-26 10:57:50 +02:00
2021-04-08 13:47:31 +02:00
func tableView ( _ tableView : UITableView , didSelectRowAt indexPath : IndexPath ) {
2021-04-26 10:57:50 +02:00
guard let dataSource = viewModel . dataSource else { return }
2021-06-01 08:07:44 +02:00
guard let item = dataSource . itemIdentifier ( for : indexPath ) else { return }
2021-04-26 10:57:50 +02:00
2021-04-14 10:41:30 +02:00
switch item {
2021-06-01 08:07:44 +02:00
case . apperance :
// d o n o t h i n g
break
case . notification :
// d o n o t h i n g
break
case . boringZone ( let link ) , . spicyZone ( let link ) :
switch link {
case . termsOfService , . privacyPolicy :
// s a m e U R L
guard let url = viewModel . privacyURL else { break }
coordinator . present (
scene : . safari ( url : url ) ,
from : self ,
transition : . safariPresent ( animated : true , completion : nil )
)
case . clearMediaCache :
context . purgeCache ( )
. receive ( on : RunLoop . main )
. sink { [ weak self ] byteCount in
guard let self = self else { return }
let byteCountformatted = AppContext . byteCountFormatter . string ( fromByteCount : Int64 ( byteCount ) )
let alertController = UIAlertController (
title : L10n . Common . Alerts . CleanCache . title ,
message : L10n . Common . Alerts . CleanCache . message ( byteCountformatted ) ,
preferredStyle : . alert
)
let okAction = UIAlertAction ( title : L10n . Common . Controls . Actions . ok , style : . default , handler : nil )
alertController . addAction ( okAction )
self . coordinator . present ( scene : . alertController ( alertController : alertController ) , from : nil , transition : . alertController ( animated : true , completion : nil ) )
}
. store ( in : & disposeBag )
case . signOut :
2021-04-14 10:41:30 +02:00
alertToSignout ( )
}
2021-04-08 13:47:31 +02:00
}
}
}
// U p d a t e s e t t i n g i n t o c o r e d a t a
extension SettingsViewController {
2021-04-26 10:57:50 +02:00
func updateTrigger ( policy : Mastodon . API . Subscriptions . Policy ) {
let objectID = self . viewModel . setting . value . objectID
let managedObjectContext = context . backgroundManagedObjectContext
2021-04-08 13:47:31 +02:00
2021-04-26 10:57:50 +02:00
managedObjectContext . performChanges {
let setting = managedObjectContext . object ( with : objectID ) as ! Setting
let ( subscription , _ ) = APIService . CoreData . createOrFetchSubscription (
into : managedObjectContext ,
setting : setting ,
policy : policy
)
let now = Date ( )
subscription . update ( activedAt : now )
setting . didUpdate ( at : now )
2021-04-08 13:47:31 +02:00
}
2021-04-26 10:57:50 +02:00
. sink { _ in
// d o n o t h i n g
} receiveValue : { _ in
// d o n o h t i n g
2021-04-08 13:47:31 +02:00
}
2021-04-26 10:57:50 +02:00
. store ( in : & disposeBag )
2021-04-08 13:47:31 +02:00
}
}
2021-04-26 10:57:50 +02:00
// MARK: - S e t t i n g s A p p e a r a n c e T a b l e V i e w C e l l D e l e g a t e
2021-04-08 13:47:31 +02:00
extension SettingsViewController : SettingsAppearanceTableViewCellDelegate {
2021-04-26 10:57:50 +02:00
func settingsAppearanceCell ( _ 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 let . apperance ( settingObjectID ) = item else { return }
2021-04-08 13:47:31 +02:00
context . managedObjectContext . performChanges {
2021-04-26 10:57:50 +02:00
let setting = self . context . managedObjectContext . object ( with : settingObjectID ) as ! Setting
setting . update ( appearanceRaw : appearanceMode . rawValue )
2021-04-08 13:47:31 +02:00
}
2021-04-26 10:57:50 +02:00
. sink { _ in
// d o n o t h i n g
2021-04-08 13:47:31 +02:00
} . store ( in : & disposeBag )
}
}
extension SettingsViewController : SettingsToggleCellDelegate {
2021-04-26 10:57:50 +02:00
func settingsToggleCell ( _ cell : SettingsToggleTableViewCell , switchValueDidChange switch : UISwitch ) {
guard let dataSource = viewModel . dataSource else { return }
guard let indexPath = tableView . indexPath ( for : cell ) else { return }
let item = dataSource . itemIdentifier ( for : indexPath )
switch item {
case . notification ( let settingObjectID , let switchMode ) :
let isOn = ` switch ` . isOn
let managedObjectContext = context . backgroundManagedObjectContext
managedObjectContext . performChanges {
let setting = managedObjectContext . object ( with : settingObjectID ) as ! Setting
guard let subscription = setting . activeSubscription else { return }
let alert = subscription . alert
switch switchMode {
case . favorite : alert . update ( favourite : isOn )
case . follow : alert . update ( follow : isOn )
case . reblog : alert . update ( reblog : isOn )
case . mention : alert . update ( mention : isOn )
}
// t r i g g e r s e t t i n g u p d a t e
alert . subscription . setting ? . didUpdate ( at : Date ( ) )
}
. sink { _ in
// d o n o t h i n g
}
. store ( in : & disposeBag )
default :
break
}
2021-04-08 13:47:31 +02:00
}
}
extension SettingsViewController : ActiveLabelDelegate {
func activeLabel ( _ activeLabel : ActiveLabel , didSelectActiveEntity entity : ActiveEntity ) {
coordinator . present (
2021-04-13 10:22:41 +02:00
scene : . safari ( url : URL ( string : " https://github.com/tootsuite/mastodon " ) ! ) ,
2021-04-08 13:47:31 +02:00
from : self ,
2021-04-13 10:22:41 +02:00
transition : . safariPresent ( animated : true , completion : nil )
)
2021-04-08 13:47:31 +02:00
}
}
2021-05-19 10:45:01 +02:00
// MARK: - U I A d a p t i v e P r e s e n t a t i o n C o n t r o l l e r D e l e g a t e
extension SettingsViewController : UIAdaptivePresentationControllerDelegate {
func adaptivePresentationStyle ( for controller : UIPresentationController , traitCollection : UITraitCollection ) -> UIModalPresentationStyle {
return . formSheet
}
}
extension SettingsViewController {
var closeKeyCommand : UIKeyCommand {
UIKeyCommand (
title : L10n . Scene . Settings . Keyboard . closeSettingsWindow ,
image : nil ,
action : #selector ( SettingsViewController . closeSettingsWindowKeyCommandHandler ( _ : ) ) ,
input : " w " ,
modifierFlags : . command ,
propertyList : nil ,
alternates : [ ] ,
discoverabilityTitle : nil ,
attributes : [ ] ,
state : . off
)
}
override var keyCommands : [ UIKeyCommand ] ? {
return [ closeKeyCommand ]
}
@objc private func closeSettingsWindowKeyCommandHandler ( _ sender : UIKeyCommand ) {
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
dismiss ( animated : true , completion : nil )
}
}
2021-04-08 13:47:31 +02:00
#if canImport ( SwiftUI ) && DEBUG
import SwiftUI
struct SettingsViewController_Previews : PreviewProvider {
static var previews : some View {
Group {
UIViewControllerPreview { ( ) -> UIViewController in
return SettingsViewController ( )
}
. previewLayout ( . fixed ( width : 390 , height : 844 ) )
}
}
}
#endif