Started refactoring the conversation image picker screens

Fixed a bug where returning from the background on the conversation screen would result in the input view being hidden
Refactored the PhotoCollectionPickerViewController to use the SettingsTableViewController convention
Updated the SettingsTableViewModel to worked based on Combine instead of the DatabaseObservable so it's more reusable for non-db cases
pull/672/head
Morgan Pretty 3 years ago
parent face9da02b
commit c707a2f80c

@ -10,7 +10,6 @@
1FFD68A448D5A1439F2F02FD /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DBA125424EDD2417B515C63A /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework */; };
3289CA2E9E89DA9D4D52A90C /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BF4561630A52BE96F164CF6 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework */; };
340FC8B6204DAC8D007AEB0F /* OWSQRCodeScanningViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC896204DAC8C007AEB0F /* OWSQRCodeScanningViewController.m */; };
340FC8B7204DAC8D007AEB0F /* OWSConversationSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC89A204DAC8D007AEB0F /* OWSConversationSettingsViewController.m */; };
3427C64320F500E000EEC730 /* OWSMessageTimerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3427C64220F500DF00EEC730 /* OWSMessageTimerView.m */; };
3430FE181F7751D4000EC51B /* GiphyAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3430FE171F7751D4000EC51B /* GiphyAPI.swift */; };
34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34330AA21E79686200DF2FB9 /* OWSProgressView.m */; };
@ -21,7 +20,6 @@
347850551FD749C0007B8332 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6F509951AA53F760068F56A /* Localizable.strings */; };
3488F9362191CC4000E524CC /* MediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3488F9352191CC4000E524CC /* MediaView.swift */; };
3496955C219B605E00DCFE74 /* ImagePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34969559219B605E00DCFE74 /* ImagePickerController.swift */; };
3496955D219B605E00DCFE74 /* PhotoCollectionPickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496955A219B605E00DCFE74 /* PhotoCollectionPickerController.swift */; };
3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496955B219B605E00DCFE74 /* PhotoLibrary.swift */; };
3496956021A2FC8100DCFE74 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3496955F21A2FC8100DCFE74 /* CloudKit.framework */; };
34A6C28021E503E700B5B12E /* OWSImagePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A6C27F21E503E600B5B12E /* OWSImagePickerController.swift */; };
@ -419,8 +417,6 @@
C38EF35D255B6DCC007E1867 /* OWSNavigationController.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF343255B6DC5007E1867 /* OWSNavigationController.h */; settings = {ATTRIBUTES = (Public, ); }; };
C38EF35E255B6DCC007E1867 /* OWSViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF344255B6DC5007E1867 /* OWSViewController.h */; settings = {ATTRIBUTES = (Public, ); }; };
C38EF363255B6DCC007E1867 /* ModalActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF349255B6DC7007E1867 /* ModalActivityIndicatorViewController.swift */; };
C38EF365255B6DCC007E1867 /* OWSTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF34B255B6DC8007E1867 /* OWSTableViewController.m */; };
C38EF367255B6DCC007E1867 /* OWSTableViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF34D255B6DC8007E1867 /* OWSTableViewController.h */; settings = {ATTRIBUTES = (Public, ); }; };
C38EF36F255B6DCC007E1867 /* OWSViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF355255B6DCB007E1867 /* OWSViewController.m */; };
C38EF370255B6DCC007E1867 /* OWSNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF356255B6DCB007E1867 /* OWSNavigationController.m */; };
C38EF372255B6DCC007E1867 /* MediaMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF358255B6DCC007E1867 /* MediaMessageView.swift */; };
@ -717,6 +713,7 @@
FD7115FE28C8202D00B47552 /* ReplaySubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115FD28C8202D00B47552 /* ReplaySubject.swift */; };
FD71160028C8253500B47552 /* UIView+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115FF28C8253500B47552 /* UIView+Combine.swift */; };
FD71160228C8255900B47552 /* UIControl+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71160128C8255900B47552 /* UIControl+Combine.swift */; };
FD71160428C95B5600B47552 /* PhotoCollectionPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71160328C95B5600B47552 /* PhotoCollectionPickerViewModel.swift */; };
FD7162DB281B6C440060647B /* TypedTableAlias.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7162DA281B6C440060647B /* TypedTableAlias.swift */; };
FD716E6428502DDD00C96BF4 /* CallManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD716E6328502DDD00C96BF4 /* CallManagerProtocol.swift */; };
FD716E6628502EE200C96BF4 /* CurrentCallProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD716E6528502EE200C96BF4 /* CurrentCallProtocol.swift */; };
@ -1054,9 +1051,6 @@
29CF8C79F41BF00B1C2E59A0 /* Pods-SessionUIKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionUIKit.app store release.xcconfig"; path = "Target Support Files/Pods-SessionUIKit/Pods-SessionUIKit.app store release.xcconfig"; sourceTree = "<group>"; };
340FC888204DAC8C007AEB0F /* OWSQRCodeScanningViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSQRCodeScanningViewController.h; sourceTree = "<group>"; };
340FC896204DAC8C007AEB0F /* OWSQRCodeScanningViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSQRCodeScanningViewController.m; sourceTree = "<group>"; };
340FC899204DAC8D007AEB0F /* OWSConversationSettingsViewDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSConversationSettingsViewDelegate.h; sourceTree = "<group>"; };
340FC89A204DAC8D007AEB0F /* OWSConversationSettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSConversationSettingsViewController.m; sourceTree = "<group>"; };
340FC8A0204DAC8D007AEB0F /* OWSConversationSettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSConversationSettingsViewController.h; sourceTree = "<group>"; };
3427C64120F500DE00EEC730 /* OWSMessageTimerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageTimerView.h; sourceTree = "<group>"; };
3427C64220F500DF00EEC730 /* OWSMessageTimerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageTimerView.m; sourceTree = "<group>"; };
3430FE171F7751D4000EC51B /* GiphyAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GiphyAPI.swift; sourceTree = "<group>"; };
@ -1069,7 +1063,6 @@
346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropScaleImageViewController.swift; sourceTree = "<group>"; };
3488F9352191CC4000E524CC /* MediaView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = "<group>"; };
34969559219B605E00DCFE74 /* ImagePickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerController.swift; sourceTree = "<group>"; };
3496955A219B605E00DCFE74 /* PhotoCollectionPickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCollectionPickerController.swift; sourceTree = "<group>"; };
3496955B219B605E00DCFE74 /* PhotoLibrary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoLibrary.swift; sourceTree = "<group>"; };
3496955F21A2FC8100DCFE74 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
34A6C27F21E503E600B5B12E /* OWSImagePickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSImagePickerController.swift; sourceTree = "<group>"; };
@ -1520,8 +1513,6 @@
C38EF343255B6DC5007E1867 /* OWSNavigationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSNavigationController.h; path = "SignalUtilitiesKit/Shared View Controllers/OWSNavigationController.h"; sourceTree = SOURCE_ROOT; };
C38EF344255B6DC5007E1867 /* OWSViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSViewController.h; path = "SignalUtilitiesKit/Shared View Controllers/OWSViewController.h"; sourceTree = SOURCE_ROOT; };
C38EF349255B6DC7007E1867 /* ModalActivityIndicatorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ModalActivityIndicatorViewController.swift; path = "SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift"; sourceTree = SOURCE_ROOT; };
C38EF34B255B6DC8007E1867 /* OWSTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSTableViewController.m; path = "SignalUtilitiesKit/Shared View Controllers/OWSTableViewController.m"; sourceTree = SOURCE_ROOT; };
C38EF34D255B6DC8007E1867 /* OWSTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSTableViewController.h; path = "SignalUtilitiesKit/Shared View Controllers/OWSTableViewController.h"; sourceTree = SOURCE_ROOT; };
C38EF355255B6DCB007E1867 /* OWSViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSViewController.m; path = "SignalUtilitiesKit/Shared View Controllers/OWSViewController.m"; sourceTree = SOURCE_ROOT; };
C38EF356255B6DCB007E1867 /* OWSNavigationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSNavigationController.m; path = "SignalUtilitiesKit/Shared View Controllers/OWSNavigationController.m"; sourceTree = SOURCE_ROOT; };
C38EF358255B6DCC007E1867 /* MediaMessageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MediaMessageView.swift; path = "SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift"; sourceTree = SOURCE_ROOT; };
@ -1806,6 +1797,7 @@
FD7115FD28C8202D00B47552 /* ReplaySubject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplaySubject.swift; sourceTree = "<group>"; };
FD7115FF28C8253500B47552 /* UIView+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Combine.swift"; sourceTree = "<group>"; };
FD71160128C8255900B47552 /* UIControl+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIControl+Combine.swift"; sourceTree = "<group>"; };
FD71160328C95B5600B47552 /* PhotoCollectionPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoCollectionPickerViewModel.swift; sourceTree = "<group>"; };
FD7162DA281B6C440060647B /* TypedTableAlias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedTableAlias.swift; sourceTree = "<group>"; };
FD716E6328502DDD00C96BF4 /* CallManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManagerProtocol.swift; sourceTree = "<group>"; };
FD716E6528502EE200C96BF4 /* CurrentCallProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentCallProtocol.swift; sourceTree = "<group>"; };
@ -2681,9 +2673,6 @@
isa = PBXGroup;
children = (
FD7115EC28C5D79100B47552 /* Views */,
340FC8A0204DAC8D007AEB0F /* OWSConversationSettingsViewController.h */,
340FC89A204DAC8D007AEB0F /* OWSConversationSettingsViewController.m */,
340FC899204DAC8D007AEB0F /* OWSConversationSettingsViewDelegate.h */,
B84A89BB25DE328A0040017D /* ProfilePictureVC.swift */,
3427C64120F500DE00EEC730 /* OWSMessageTimerView.h */,
3427C64220F500DF00EEC730 /* OWSMessageTimerView.m */,
@ -3002,7 +2991,7 @@
346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */,
34969559219B605E00DCFE74 /* ImagePickerController.swift */,
34A6C27F21E503E600B5B12E /* OWSImagePickerController.swift */,
3496955A219B605E00DCFE74 /* PhotoCollectionPickerController.swift */,
FD71160328C95B5600B47552 /* PhotoCollectionPickerViewModel.swift */,
3496955B219B605E00DCFE74 /* PhotoLibrary.swift */,
4CA485BA2232339F004B9E7D /* PhotoCaptureViewController.swift */,
4C21D5D7223AC60F00EF8A77 /* PhotoCapture.swift */,
@ -3061,8 +3050,6 @@
C38EF349255B6DC7007E1867 /* ModalActivityIndicatorViewController.swift */,
C38EF343255B6DC5007E1867 /* OWSNavigationController.h */,
C38EF356255B6DCB007E1867 /* OWSNavigationController.m */,
C38EF34D255B6DC8007E1867 /* OWSTableViewController.h */,
C38EF34B255B6DC8007E1867 /* OWSTableViewController.m */,
C38EF344255B6DC5007E1867 /* OWSViewController.h */,
C38EF355255B6DCB007E1867 /* OWSViewController.m */,
C38EF33F255B6DC5007E1867 /* SheetViewController.swift */,
@ -4166,7 +4153,6 @@
C38EF3F5255B6DF7007E1867 /* OWSTextField.h in Headers */,
C33FDDB3255A582000E217F9 /* OWSError.h in Headers */,
C38EF35E255B6DCC007E1867 /* OWSViewController.h in Headers */,
C38EF367255B6DCC007E1867 /* OWSTableViewController.h in Headers */,
C38EF246255B6D67007E1867 /* UIFont+OWS.h in Headers */,
C33FD9AF255A548A00E217F9 /* SignalUtilitiesKit.h in Headers */,
C33FDC50255A582000E217F9 /* OWSDispatch.h in Headers */,
@ -5137,7 +5123,6 @@
C38EF387255B6DD2007E1867 /* AttachmentItemCollection.swift in Sources */,
C38EF22B255B6D5D007E1867 /* ShareViewDelegate.swift in Sources */,
C38EF3BF255B6DE7007E1867 /* ImageEditorView.swift in Sources */,
C38EF365255B6DCC007E1867 /* OWSTableViewController.m in Sources */,
C38EF40C255B6DF7007E1867 /* GradientView.swift in Sources */,
C38EF3FA255B6DF7007E1867 /* DirectionalPanGestureRecognizer.swift in Sources */,
C38EF3BB255B6DE7007E1867 /* ImageEditorStrokeItem.swift in Sources */,
@ -5545,7 +5530,6 @@
7B9F71D12852EEE2006DFE7B /* EmojiWithSkinTones+String.swift in Sources */,
34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */,
B886B4A92398BA1500211ABE /* QRCode.swift in Sources */,
3496955D219B605E00DCFE74 /* PhotoCollectionPickerController.swift in Sources */,
34B0796D1FCF46B100E248C2 /* MainAppContext.m in Sources */,
34A8B3512190A40E00218A25 /* MediaAlbumView.swift in Sources */,
FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */,
@ -5590,6 +5574,7 @@
C374EEEB25DA3CA70073A857 /* ConversationTitleView.swift in Sources */,
FD716E7128505E5200C96BF4 /* MessageRequestsCell.swift in Sources */,
B88FA7F2260C3EB10049422F /* OpenGroupSuggestionGrid.swift in Sources */,
FD71160428C95B5600B47552 /* PhotoCollectionPickerViewModel.swift in Sources */,
FD37EA1928AC5CCA003AE748 /* NotificationSoundViewModel.swift in Sources */,
FD7115EE28C5D79B00B47552 /* SettingsAvatarCell.swift in Sources */,
4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */,
@ -5642,7 +5627,6 @@
7B9F71D42852EEE2006DFE7B /* Emoji+Name.swift in Sources */,
4CA46F4C219CCC630038ABDE /* CaptionView.swift in Sources */,
C328253025CA55370062D0A7 /* ContextMenuWindow.swift in Sources */,
340FC8B7204DAC8D007AEB0F /* OWSConversationSettingsViewController.m in Sources */,
34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */,
B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */,
7B9F71D72853100A006DFE7B /* Emoji+Available.swift in Sources */,

@ -422,6 +422,15 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
@objc func applicationDidBecomeActive(_ notification: Notification) {
startObservingChanges(didReturnFromBackground: true)
recoverInputView()
if !isShowingSearchUI {
if !self.isFirstResponder {
self.becomeFirstResponder()
}
else {
self.reloadInputViews()
}
}
}
@objc func applicationDidResignActive(_ notification: Notification) {

@ -141,6 +141,7 @@ class ThreadDisappearingMessagesViewModel: SettingsTableViewModel<ThreadDisappea
]
}
.removeDuplicates()
.publisher(in: Storage.shared)
// MARK: - Functions

@ -531,6 +531,7 @@ class ThreadSettingsViewModel: SettingsTableViewModel<ThreadSettingsViewModel.Na
]
}
.removeDuplicates()
.publisher(in: Storage.shared)
// MARK: - Functions

@ -19,7 +19,7 @@ protocol ImagePickerGridControllerDelegate: AnyObject {
func imagePickerCanSelectAdditionalItems(_ imagePicker: ImagePickerGridController) -> Bool
}
class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegate, PhotoCollectionPickerDelegate {
class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegate {
weak var delegate: ImagePickerGridControllerDelegate?
@ -416,10 +416,26 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
var isShowingCollectionPickerController: Bool = false
lazy var collectionPickerController: PhotoCollectionPickerController = {
return PhotoCollectionPickerController(library: library,
collectionDelegate: self)
}()
lazy var collectionPickerController: SettingsTableViewController = SettingsTableViewController(
viewModel: PhotoCollectionPickerViewModel(library: library) { [weak self] collection in
guard self?.photoCollection != collection else {
self?.hideCollectionPicker()
return
}
// Any selections are invalid as they refer to indices in a different collection
self?.clearCollectionViewSelection()
self?.photoCollection = collection
self?.photoCollectionContents = collection.contents()
self?.titleView.text = collection.localizedTitle()
self?.collectionView?.reloadData()
self?.scrollToBottom(animated: false)
self?.hideCollectionPicker()
}
)
func showCollectionPicker() {
Logger.debug("")
@ -437,7 +453,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
collectionPickerView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top)
collectionPickerView.autoPinEdge(toSuperviewSafeArea: .top)
collectionPickerView.layoutIfNeeded()
collectionPickerView.backgroundColor = .white
// Initially position offscreen, we'll animate it in.
collectionPickerView.frame = collectionPickerView.frame.offsetBy(dx: 0, dy: collectionPickerView.frame.height)
@ -463,27 +478,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
}.retainUntilComplete()
}
// MARK: - PhotoCollectionPickerDelegate
func photoCollectionPicker(_ photoCollectionPicker: PhotoCollectionPickerController, didPickCollection collection: PhotoCollection) {
guard photoCollection != collection else {
hideCollectionPicker()
return
}
// Any selections are invalid as they refer to indices in a different collection
clearCollectionViewSelection()
photoCollection = collection
photoCollectionContents = photoCollection.contents()
titleView.text = photoCollection.localizedTitle()
collectionView?.reloadData()
scrollToBottom(animated: false)
hideCollectionPicker()
}
// MARK: - UICollectionView
override func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {

@ -1,138 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import Foundation
import Photos
import PromiseKit
protocol PhotoCollectionPickerDelegate: AnyObject {
func photoCollectionPicker(_ photoCollectionPicker: PhotoCollectionPickerController, didPickCollection collection: PhotoCollection)
}
class PhotoCollectionPickerController: OWSTableViewController, PhotoLibraryDelegate {
private weak var collectionDelegate: PhotoCollectionPickerDelegate?
private let library: PhotoLibrary
private var photoCollections: [PhotoCollection]
required init(library: PhotoLibrary,
collectionDelegate: PhotoCollectionPickerDelegate) {
self.library = library
self.photoCollections = library.allPhotoCollections()
self.collectionDelegate = collectionDelegate
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
tableView.backgroundColor = .white
tableView.separatorColor = .clear
library.add(delegate: self)
updateContents()
}
// MARK: -
private func updateContents() {
photoCollections = library.allPhotoCollections()
let sectionItems = photoCollections.map { collection in
return OWSTableItem(customCellBlock: { self.buildTableCell(collection: collection) },
customRowHeight: UITableView.automaticDimension,
actionBlock: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.didSelectCollection(collection: collection)
})
}
let section = OWSTableSection(title: nil, items: sectionItems)
let contents = OWSTableContents()
contents.addSection(section)
self.contents = contents
}
private let numberFormatter: NumberFormatter = NumberFormatter()
private func buildTableCell(collection: PhotoCollection) -> UITableViewCell {
let cell = OWSTableItem.newCell()
cell.backgroundColor = .white
cell.contentView.backgroundColor = .white
cell.selectedBackgroundView?.backgroundColor = UIColor(white: 0.2, alpha: 1)
let contents = collection.contents()
let titleLabel = UILabel()
titleLabel.text = collection.localizedTitle()
titleLabel.font = .systemFont(ofSize: Values.mediumFontSize)
titleLabel.textColor = .black
let countLabel = UILabel()
countLabel.text = numberFormatter.string(for: contents.assetCount)
countLabel.font = .systemFont(ofSize: Values.smallFontSize)
countLabel.textColor = .black
let textStack = UIStackView(arrangedSubviews: [titleLabel, countLabel])
textStack.axis = .vertical
textStack.alignment = .leading
textStack.spacing = 2
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
let kImageSize = 80
imageView.autoSetDimensions(to: CGSize(width: kImageSize, height: kImageSize))
let hStackView = UIStackView(arrangedSubviews: [imageView, textStack])
hStackView.axis = .horizontal
hStackView.alignment = .center
hStackView.spacing = Values.mediumSpacing
let photoMediaSize = PhotoMediaSize(thumbnailSize: CGSize(width: kImageSize, height: kImageSize))
if let assetItem = contents.lastAssetItem(photoMediaSize: photoMediaSize) {
assetItem.asyncThumbnail { [weak imageView] image in
AssertIsOnMainThread()
guard let imageView = imageView else {
return
}
guard let image = image else {
owsFailDebug("image was unexpectedly nil")
return
}
imageView.image = image
}
}
cell.contentView.addSubview(hStackView)
hStackView.ows_autoPinToSuperviewMargins()
return cell
}
// MARK: Actions
func didSelectCollection(collection: PhotoCollection) {
collectionDelegate?.photoCollectionPicker(self, didPickCollection: collection)
}
// MARK: PhotoLibraryDelegate
func photoLibraryDidChange(_ photoLibrary: PhotoLibrary) {
updateContents()
}
}

@ -0,0 +1,90 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import GRDB
import DifferenceKit
import SessionUIKit
import SessionMessagingKit
import SessionUtilitiesKit
class PhotoCollectionPickerViewModel: SettingsTableViewModel<NoNav, PhotoCollectionPickerViewModel.Section, PhotoCollectionPickerViewModel.Item> {
// MARK: - Config
public enum Section: SettingSection {
case content
}
public struct Item: Equatable, Hashable, Differentiable {
let id: String
}
private let library: PhotoLibrary
private let onCollectionSelected: (PhotoCollection) -> Void
private var photoCollections: CurrentValueSubject<[PhotoCollection], Error>
// MARK: - Initialization
init(library: PhotoLibrary, onCollectionSelected: @escaping (PhotoCollection) -> Void) {
self.library = library
self.onCollectionSelected = onCollectionSelected
self.photoCollections = CurrentValueSubject(library.allPhotoCollections())
}
// MARK: - Content
override var title: String { "NOTIFICATIONS_STYLE_SOUND_TITLE".localized() }
private var _settingsData: [SectionModel] = []
public override var settingsData: [SectionModel] { _settingsData }
public override var observableSettingsData: ObservableData { _observableSettingsData }
private lazy var _observableSettingsData: ObservableData = {
self.photoCollections
.map { collections in
[
SectionModel(
model: .content,
elements: collections.map { collection in
let contents: PhotoCollectionContents = collection.contents()
let photoMediaSize: PhotoMediaSize = PhotoMediaSize(
thumbnailSize: CGSize(width: IconSize.large.size, height: IconSize.large.size)
)
let lastAssetItem: PhotoPickerAssetItem? = contents.lastAssetItem(photoMediaSize: photoMediaSize)
return SettingInfo(
id: Item(id: collection.id),
iconSize: .large,
iconSetter: { imageView in
// Note: We need to capture 'lastAssetItem' otherwise it'll be released and we won't
// be able to load the thumbnail
lastAssetItem?.asyncThumbnail { [weak imageView] image in
imageView?.image = image
}
},
title: collection.localizedTitle(),
subtitle: "\(contents.assetCount)",
action: .trigger(showChevron: false) { [weak self] in
self?.onCollectionSelected(collection)
}
)
}
)
]
}
.removeDuplicates()
.eraseToAnyPublisher()
}()
// MARK: - Functions
public override func updateSettings(_ updatedSettings: [SectionModel]) {
self._settingsData = updatedSettings
}
// MARK: PhotoLibraryDelegate
func photoLibraryDidChange(_ photoLibrary: PhotoLibrary) {
self.photoCollections.send(library.allPhotoCollections())
}
}

@ -216,13 +216,15 @@ class PhotoCollectionContents {
}
class PhotoCollection {
public let id: String
private let collection: PHAssetCollection
// The user never sees this collection, but we use it for a null object pattern
// when the user has denied photos access.
static let empty = PhotoCollection(collection: PHAssetCollection())
static let empty = PhotoCollection(id: "", collection: PHAssetCollection())
init(collection: PHAssetCollection) {
init(id: String, collection: PHAssetCollection) {
self.id = id
self.collection = collection
}
@ -289,7 +291,7 @@ class PhotoLibrary: NSObject, PHPhotoLibraryChangeObserver {
subtype: .smartAlbumUserLibrary,
options: fetchOptions
).enumerateObjects { collection, _, stop in
fetchedCollection = PhotoCollection(collection: collection)
fetchedCollection = PhotoCollection(id: collection.localIdentifier, collection: collection)
stop.pointee = true
}
@ -310,17 +312,16 @@ class PhotoLibrary: NSObject, PHPhotoLibraryChangeObserver {
let (collection, hideIfEmpty) = arg
// De-duplicate by id.
let collectionId = collection.localIdentifier
guard !collectionIds.contains(collectionId) else {
return
}
let collectionId: String = collection.localIdentifier
guard !collectionIds.contains(collectionId) else { return }
collectionIds.insert(collectionId)
guard let assetCollection = collection as? PHAssetCollection else {
owsFailDebug("Asset collection has unexpected type: \(type(of: collection))")
return
}
let photoCollection = PhotoCollection(collection: assetCollection)
let photoCollection = PhotoCollection(id: collectionId, collection: assetCollection)
guard !hideIfEmpty || photoCollection.contents().assetCount > 0 else {
return
}

@ -89,6 +89,7 @@ class ConversationSettingsViewModel: SettingsTableViewModel<NoNav, ConversationS
]
}
.removeDuplicates()
.publisher(in: Storage.shared)
// MARK: - Functions

@ -120,6 +120,7 @@ class HelpViewModel: SettingsTableViewModel<NoNav, HelpViewModel.Section, HelpVi
]
}
.removeDuplicates()
.publisher(in: Storage.shared)
// MARK: - Functions

@ -59,6 +59,7 @@ class NotificationContentViewModel: SettingsTableViewModel<NoNav, NotificationSe
]
}
.removeDuplicates()
.publisher(in: Storage.shared)
// MARK: - Functions

@ -140,6 +140,7 @@ class NotificationSettingsViewModel: SettingsTableViewModel<NoNav, NotificationS
]
}
.removeDuplicates()
.publisher(in: Storage.shared)
// MARK: - Functions

@ -159,6 +159,7 @@ class NotificationSoundViewModel: SettingsTableViewModel<NotificationSoundViewMo
]
}
.removeDuplicates()
.publisher(in: Storage.shared)
// MARK: - Functions

@ -170,6 +170,7 @@ class PrivacySettingsViewModel: SettingsTableViewModel<PrivacySettingsViewModel.
]
}
.removeDuplicates()
.publisher(in: Storage.shared)
// MARK: - Functions

@ -16,8 +16,9 @@ class SettingsTableViewController<NavItemId: Equatable, Section: SettingSection,
typealias SectionModel = SettingsTableViewModel<NavItemId, Section, SettingItem>.SectionModel
private let viewModel: SettingsTableViewModel<NavItemId, Section, SettingItem>
private var dataChangeObservable: DatabaseCancellable?
private var hasLoadedInitialSettingsData: Bool = false
private var dataStreamJustFailed: Bool = false
private var dataChangeCancellable: AnyCancellable?
private var disposables: Set<AnyCancellable> = Set()
public var viewModelType: AnyObject.Type { return type(of: viewModel) }
@ -119,26 +120,42 @@ class SettingsTableViewController<NavItemId: Equatable, Section: SettingSection,
private func startObservingChanges() {
// Start observing for data changes
dataChangeObservable = Storage.shared.start(
viewModel.observableSettingsData,
// If we haven't done the initial load the trigger it immediately (blocking the main
// thread so we remain on the launch screen until it completes to be consistent with
// the old behaviour)
scheduling: (hasLoadedInitialSettingsData ?
.async(onQueue: .main) :
.immediate
),
onError: { _ in },
onChange: { [weak self] settingsData in
// The default scheduler emits changes on the main thread
self?.handleSettingsUpdates(settingsData)
}
)
dataChangeCancellable = viewModel.observableSettingsData
.receiveOnMain(
// If we haven't done the initial load the trigger it immediately (blocking the main
// thread so we remain on the launch screen until it completes to be consistent with
// the old behaviour)
immediately: !hasLoadedInitialSettingsData
)
.sink(
receiveCompletion: { [weak self] result in
switch result {
case .failure(let error):
let title: String = (self?.viewModel.title ?? "unknown")
// If we got an error then try to restart the stream once, otherwise log the error
guard self?.dataStreamJustFailed == false else {
SNLog("Unable to recover database stream in '\(title)' settings with error: \(error)")
return
}
SNLog("Atempting recovery for database stream in '\(title)' settings with error: \(error)")
self?.dataStreamJustFailed = true
self?.startObservingChanges()
case .finished: break
}
},
receiveValue: { [weak self] settingsData in
self?.dataStreamJustFailed = false
self?.handleSettingsUpdates(settingsData)
}
)
}
private func stopObservingChanges() {
// Stop observing database changes
dataChangeObservable?.cancel()
dataChangeCancellable?.cancel()
}
private func handleSettingsUpdates(_ updatedData: [SectionModel], initialLoad: Bool = false) {
@ -188,7 +205,7 @@ class SettingsTableViewController<NavItemId: Equatable, Section: SettingSection,
.store(in: &disposables)
viewModel.leftNavItems
.receiveOnMainImmediately()
.receiveOnMain(immediately: true)
.sink { [weak self] maybeItems in
self?.navigationItem.setLeftBarButtonItems(
maybeItems.map { items in
@ -210,7 +227,7 @@ class SettingsTableViewController<NavItemId: Equatable, Section: SettingSection,
.store(in: &disposables)
viewModel.rightNavItems
.receiveOnMainImmediately()
.receiveOnMain(immediately: true)
.sink { [weak self] maybeItems in
self?.navigationItem.setRightBarButtonItems(
maybeItems.map { items in
@ -292,6 +309,8 @@ class SettingsTableViewController<NavItemId: Equatable, Section: SettingSection,
let cell: SettingsCell = tableView.dequeue(type: SettingsCell.self, for: indexPath)
cell.update(
icon: settingInfo.icon,
iconSize: settingInfo.iconSize,
iconSetter: settingInfo.iconSetter,
title: settingInfo.title,
subtitle: settingInfo.subtitle,
alignment: settingInfo.alignment,
@ -314,9 +333,7 @@ class SettingsTableViewController<NavItemId: Equatable, Section: SettingSection,
switch section.model.style {
case .none:
let result: UIView = UIView()
result.set(.height, to: 0)
return result
return UIView()
case .padding, .title:
let result: SettingHeaderView = tableView.dequeueHeaderFooterView(type: SettingHeaderView.self)
@ -331,6 +348,15 @@ class SettingsTableViewController<NavItemId: Equatable, Section: SettingSection,
// MARK: - UITableViewDelegate
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
let section: SectionModel = viewModel.settingsData[section]
switch section.model.style {
case .none: return 0
case .padding, .title: return UITableView.automaticDimension
}
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
@ -491,6 +517,8 @@ class SettingsTableViewController<NavItemId: Equatable, Section: SettingSection,
if let existingCell: SettingsCell = tableView.cellForRow(at: indexPath) as? SettingsCell {
existingCell.update(
icon: settingInfo.icon,
iconSize: settingInfo.iconSize,
iconSetter: settingInfo.iconSetter,
title: settingInfo.title,
subtitle: settingInfo.subtitle,
alignment: settingInfo.alignment,

@ -10,7 +10,7 @@ import SessionUtilitiesKit
class SettingsTableViewModel<NavItemId: Equatable, Section: SettingSection, SettingItem: Hashable & Differentiable> {
typealias SectionModel = ArraySection<Section, SettingInfo<SettingItem>>
typealias ObservableData = ValueObservation<ValueReducers.RemoveDuplicates<ValueReducers.Fetch<[SectionModel]>>>
typealias ObservableData = AnyPublisher<[SectionModel], Error>
var closeNavItemId: NavItemId?
@ -155,11 +155,27 @@ extension SettingSection {
var style: SettingSectionHeaderStyle { .none }
}
// MARK: - IconSize
public enum IconSize: Differentiable {
case small
case large
var size: CGFloat {
switch self {
case .small: return 24
case .large: return 80
}
}
}
// MARK: - SettingInfo
struct SettingInfo<ID: Hashable & Differentiable>: Equatable, Hashable, Differentiable {
let id: ID
let icon: UIImage?
let iconSize: IconSize
let iconSetter: ((UIImageView) -> Void)?
let title: String
let subtitle: String?
let alignment: NSTextAlignment
@ -174,6 +190,8 @@ struct SettingInfo<ID: Hashable & Differentiable>: Equatable, Hashable, Differen
init(
id: ID,
icon: UIImage? = nil,
iconSize: IconSize = .small,
iconSetter: ((UIImageView) -> Void)? = nil,
title: String,
subtitle: String? = nil,
alignment: NSTextAlignment = .left,
@ -185,6 +203,8 @@ struct SettingInfo<ID: Hashable & Differentiable>: Equatable, Hashable, Differen
) {
self.id = id
self.icon = icon
self.iconSize = iconSize
self.iconSetter = iconSetter
self.title = title
self.subtitle = subtitle
self.alignment = alignment
@ -202,6 +222,7 @@ struct SettingInfo<ID: Hashable & Differentiable>: Equatable, Hashable, Differen
func hash(into hasher: inout Hasher) {
id.hash(into: &hasher)
icon.hash(into: &hasher)
iconSize.hash(into: &hasher)
title.hash(into: &hasher)
subtitle.hash(into: &hasher)
alignment.hash(into: &hasher)
@ -213,6 +234,7 @@ struct SettingInfo<ID: Hashable & Differentiable>: Equatable, Hashable, Differen
return (
lhs.id == rhs.id &&
lhs.icon == rhs.icon &&
lhs.iconSize == rhs.iconSize &&
lhs.title == rhs.title &&
lhs.subtitle == rhs.subtitle &&
lhs.alignment == rhs.alignment &&

@ -12,6 +12,8 @@ class SettingsCell: UITableViewCell {
// MARK: - UI
private lazy var stackViewImageHeightConstraint: NSLayoutConstraint = contentStackView.heightAnchor.constraint(equalTo: iconImageView.heightAnchor)
private let topSeparator: UIView = {
let result: UIView = UIView.separator()
result.translatesAutoresizingMaskIntoConstraints = false
@ -28,12 +30,6 @@ class SettingsCell: UITableViewCell {
result.alignment = .center
result.spacing = Values.mediumSpacing
result.isLayoutMarginsRelativeArrangement = true
result.layoutMargins = UIEdgeInsets(
top: Values.mediumSpacing,
leading: Values.largeSpacing,
bottom: Values.mediumSpacing,
trailing: Values.largeSpacing
)
return result
}()
@ -229,9 +225,6 @@ class SettingsCell: UITableViewCell {
titleLabel.setCompressionResistanceHorizontalLow()
subtitleLabel.setCompressionResistanceHorizontalLow()
iconImageView.set(.width, to: 24)
iconImageView.set(.height, to: 24)
pushChevronImageView.setContentHuggingHigh()
pushChevronImageView.setCompressionResistanceHigh()
@ -337,6 +330,8 @@ class SettingsCell: UITableViewCell {
self.onExtraAction = nil
self.accessibilityIdentifier = nil
stackViewImageHeightConstraint.isActive = false
iconImageView.removeConstraints(iconImageView.constraints)
iconImageView.image = nil
titleLabel.text = ""
titleLabel.themeTextColor = .textPrimary
@ -363,6 +358,8 @@ class SettingsCell: UITableViewCell {
public func update(
icon: UIImage?,
iconSize: IconSize,
iconSetter: ((UIImageView) -> Void)?,
title: String,
subtitle: String?,
alignment: NSTextAlignment,
@ -379,15 +376,38 @@ class SettingsCell: UITableViewCell {
self.onExtraAction = onExtraAction
self.accessibilityIdentifier = accessibilityIdentifier
stackViewImageHeightConstraint.isActive = {
switch iconSize {
case .small: return false
case .large: return true // Edge-to-edge in this case
}
}()
contentStackView.layoutMargins = UIEdgeInsets(
top: Values.mediumSpacing,
leading: {
switch iconSize {
case .small: return Values.largeSpacing
case .large: return 0 // Edge-to-edge in this case
}
}(),
bottom: Values.mediumSpacing,
trailing: Values.largeSpacing
)
// Left content
iconImageView.set(.width, to: iconSize.size)
iconImageView.set(.height, to: iconSize.size)
iconImageView.image = icon
iconImageView.isHidden = (icon == nil)
iconImageView.isHidden = (icon == nil && iconSetter == nil)
titleLabel.text = title
titleLabel.textAlignment = alignment
subtitleLabel.text = subtitle
subtitleLabel.isHidden = (subtitle == nil)
extraActionButton.isHidden = (extraActionTitle == nil)
// Call the iconSetter closure if provided to set the icon
iconSetter?(iconImageView)
// Separator/background Visibility
if action.shouldHaveBackground {
self.themeBackgroundColor = .settings_tabBackground

@ -298,10 +298,10 @@ public enum Preferences {
// MARK: - AudioPlayer
public static func audioPlayer(for sound: Sound, behaviour: OWSAudioBehavior) -> OWSAudioPlayer? {
public static func audioPlayer(for sound: Sound, behavior: OWSAudioBehavior) -> OWSAudioPlayer? {
guard let soundUrl: URL = sound.soundUrl(quiet: false) else { return nil }
let player = OWSAudioPlayer(mediaUrl: soundUrl, audioBehavior: behaviour)
let player = OWSAudioPlayer(mediaUrl: soundUrl, audioBehavior: behavior)
// These two cases should loop
if sound == .callConnecting || sound == .callOutboundRinging {

@ -28,7 +28,12 @@ public extension Publisher {
/// The standard `.receive(on: DispatchQueue.main)` seems to ocassionally dispatch to the
/// next run loop before emitting data, this method checks if it's running on the main thread already and
/// if so just emits directly rather than routing via `.receive(on:)`
func receiveOnMainImmediately() -> AnyPublisher<Output, Failure> {
func receiveOnMain(immediately receiveImmediately: Bool = false) -> AnyPublisher<Output, Failure> {
guard receiveImmediately else {
return self.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
return self
.flatMap { value -> AnyPublisher<Output, Failure> in
guard Thread.isMainThread else {

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import GRDB
import PromiseKit
import SignalCoreKit
@ -26,7 +27,7 @@ public final class Storage {
public private(set) var isValid: Bool = false
public private(set) var hasCompletedMigrations: Bool = false
private var dbWriter: DatabaseWriter?
fileprivate var dbWriter: DatabaseWriter?
private var migrator: DatabaseMigrator?
private var migrationProgressUpdater: Atomic<((String, CGFloat) -> ())>?
@ -412,3 +413,16 @@ public extension Storage {
return promise
}
}
// MARK: - Combine Extensions
public extension ValueObservation {
func publisher(in storage: Storage) -> AnyPublisher<Reducer.Value, Error> {
guard storage.isValid, let dbWriter: DatabaseWriter = storage.dbWriter else {
return Fail(error: StorageError.databaseInvalid).eraseToAnyPublisher()
}
return self.publisher(in: dbWriter)
.eraseToAnyPublisher()
}
}

@ -17,7 +17,6 @@ FOUNDATION_EXPORT const unsigned char SignalUtilitiesKitVersionString[];
#import <SignalUtilitiesKit/OWSFormat.h>
#import <SignalUtilitiesKit/OWSNavigationController.h>
#import <SignalUtilitiesKit/OWSOperation.h>
#import <SignalUtilitiesKit/OWSTableViewController.h>
#import <SignalUtilitiesKit/OWSTextField.h>
#import <SignalUtilitiesKit/OWSTextView.h>
#import <SignalUtilitiesKit/OWSViewController.h>

@ -1,181 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import <SignalUtilitiesKit/OWSViewController.h>
NS_ASSUME_NONNULL_BEGIN
extern const CGFloat kOWSTable_DefaultCellHeight;
@class OWSTableItem;
@class OWSTableSection;
@interface OWSTableContents : NSObject
@property (nonatomic) NSString *title;
@property (nonatomic, nullable) NSInteger (^sectionForSectionIndexTitleBlock)(NSString *title, NSInteger index);
@property (nonatomic, nullable) NSArray<NSString *> * (^sectionIndexTitlesForTableViewBlock)(void);
@property (nonatomic, readonly) NSArray<OWSTableSection *> *sections;
- (void)addSection:(OWSTableSection *)section;
@end
#pragma mark -
@interface OWSTableSection : NSObject
@property (nonatomic, nullable) NSString *headerTitle;
@property (nonatomic, nullable) NSString *footerTitle;
@property (nonatomic, nullable) UIView *customHeaderView;
@property (nonatomic, nullable) UIView *customFooterView;
@property (nonatomic, nullable) NSNumber *customHeaderHeight;
@property (nonatomic, nullable) NSNumber *customFooterHeight;
+ (OWSTableSection *)sectionWithTitle:(nullable NSString *)title items:(NSArray<OWSTableItem *> *)items;
- (void)addItem:(OWSTableItem *)item;
- (NSUInteger)itemCount;
@end
#pragma mark -
typedef void (^OWSTableActionBlock)(void);
typedef void (^OWSTableSubPageBlock)(UIViewController *viewController);
typedef UITableViewCell *_Nonnull (^OWSTableCustomCellBlock)(void);
typedef BOOL (^OWSTableSwitchBlock)(void);
@interface OWSTableItem : NSObject
@property (nonatomic, weak) UIViewController *tableViewController;
+ (UITableViewCell *)newCell;
+ (void)configureCell:(UITableViewCell *)cell;
+ (OWSTableItem *)itemWithTitle:(NSString *)title
actionBlock:(nullable OWSTableActionBlock)actionBlock NS_SWIFT_NAME(init(title:actionBlock:));
+ (OWSTableItem *)itemWithCustomCell:(UITableViewCell *)customCell
customRowHeight:(CGFloat)customRowHeight
actionBlock:(nullable OWSTableActionBlock)actionBlock;
+ (OWSTableItem *)itemWithCustomCellBlock:(OWSTableCustomCellBlock)customCellBlock
customRowHeight:(CGFloat)customRowHeight
actionBlock:(nullable OWSTableActionBlock)actionBlock;
+ (OWSTableItem *)itemWithCustomCellBlock:(OWSTableCustomCellBlock)customCellBlock
actionBlock:(nullable OWSTableActionBlock)actionBlock;
+ (OWSTableItem *)disclosureItemWithText:(NSString *)text actionBlock:(nullable OWSTableActionBlock)actionBlock;
+ (OWSTableItem *)disclosureItemWithText:(NSString *)text
accessibilityIdentifier:(nullable NSString *)accessibilityIdentifier
actionBlock:(nullable OWSTableActionBlock)actionBlock;
+ (OWSTableItem *)disclosureItemWithText:(NSString *)text
detailText:(NSString *)detailText
actionBlock:(nullable OWSTableActionBlock)actionBlock;
+ (OWSTableItem *)disclosureItemWithText:(NSString *)text
detailText:(NSString *)detailText
accessibilityIdentifier:(nullable NSString *)accessibilityIdentifier
actionBlock:(nullable OWSTableActionBlock)actionBlock;
+ (OWSTableItem *)disclosureItemWithText:(NSString *)text
customRowHeight:(CGFloat)customRowHeight
actionBlock:(nullable OWSTableActionBlock)actionBlock;
+ (OWSTableItem *)disclosureItemWithText:(NSString *)text
accessibilityIdentifier:(nullable NSString *)accessibilityIdentifier
customRowHeight:(CGFloat)customRowHeight
actionBlock:(nullable OWSTableActionBlock)actionBlock;
+ (OWSTableItem *)checkmarkItemWithText:(NSString *)text actionBlock:(nullable OWSTableActionBlock)actionBlock;
+ (OWSTableItem *)checkmarkItemWithText:(NSString *)text
accessibilityIdentifier:(nullable NSString *)accessibilityIdentifier
actionBlock:(nullable OWSTableActionBlock)actionBlock;
+ (OWSTableItem *)itemWithText:(NSString *)text
actionBlock:(nullable OWSTableActionBlock)actionBlock
accessoryType:(UITableViewCellAccessoryType)accessoryType;
+ (OWSTableItem *)subPageItemWithText:(NSString *)text actionBlock:(nullable OWSTableSubPageBlock)actionBlock;
+ (OWSTableItem *)subPageItemWithText:(NSString *)text
customRowHeight:(CGFloat)customRowHeight
actionBlock:(nullable OWSTableSubPageBlock)actionBlock;
+ (OWSTableItem *)actionItemWithText:(NSString *)text actionBlock:(nullable OWSTableActionBlock)actionBlock;
+ (OWSTableItem *)actionItemWithText:(NSString *)text
accessibilityIdentifier:(nullable NSString *)accessibilityIdentifier
actionBlock:(nullable OWSTableActionBlock)actionBlock;
+ (OWSTableItem *)softCenterLabelItemWithText:(NSString *)text;
+ (OWSTableItem *)softCenterLabelItemWithText:(NSString *)text customRowHeight:(CGFloat)customRowHeight;
+ (OWSTableItem *)labelItemWithText:(NSString *)text;
+ (OWSTableItem *)labelItemWithText:(NSString *)text accessoryText:(NSString *)accessoryText;
+ (OWSTableItem *)longDisclosureItemWithText:(NSString *)text actionBlock:(nullable OWSTableActionBlock)actionBlock;
+ (OWSTableItem *)switchItemWithText:(NSString *)text
isOnBlock:(OWSTableSwitchBlock)isOnBlock
target:(id)target
selector:(SEL)selector;
+ (OWSTableItem *)switchItemWithText:(NSString *)text
isOnBlock:(OWSTableSwitchBlock)isOnBlock
isEnabledBlock:(OWSTableSwitchBlock)isEnabledBlock
target:(id)target
selector:(SEL)selector;
+ (OWSTableItem *)switchItemWithText:(NSString *)text
accessibilityIdentifier:(nullable NSString *)accessibilityIdentifier
isOnBlock:(OWSTableSwitchBlock)isOnBlock
isEnabledBlock:(OWSTableSwitchBlock)isEnabledBlock
target:(id)target
selector:(SEL)selector;
- (nullable UITableViewCell *)customCell;
- (NSNumber *)customRowHeight;
@end
#pragma mark -
@protocol OWSTableViewControllerDelegate <NSObject>
- (void)tableViewWillBeginDragging;
@end
#pragma mark -
@interface OWSTableViewController : OWSViewController
@property (nonatomic, weak) id<OWSTableViewControllerDelegate> delegate;
@property (nonatomic) OWSTableContents *contents;
@property (nonatomic, readonly) UITableView *tableView;
@property (nonatomic) UITableViewStyle tableViewStyle;
- (instancetype)init NS_DESIGNATED_INITIALIZER;
#pragma mark - Presentation
- (void)presentFromViewController:(UIViewController *)fromViewController;
- (void)applyTheme;
@end
NS_ASSUME_NONNULL_END

@ -1,820 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "OWSTableViewController.h"
#import "OWSNavigationController.h"
#import "UIColor+OWS.h"
#import "UIFont+OWS.h"
#import "UIView+OWS.h"
#import <SessionUIKit/SessionUIKit.h>
NS_ASSUME_NONNULL_BEGIN
const CGFloat kOWSTable_DefaultCellHeight = 45.f;
@interface OWSTableContents ()
@property (nonatomic) NSMutableArray<OWSTableSection *> *sections;
@end
#pragma mark -
@implementation OWSTableContents
- (instancetype)init
{
if (self = [super init]) {
_sections = [NSMutableArray new];
}
return self;
}
- (void)addSection:(OWSTableSection *)section
{
OWSAssertDebug(section);
[_sections addObject:section];
}
@end
#pragma mark -
@interface OWSTableSection ()
@property (nonatomic) NSMutableArray<OWSTableItem *> *items;
@end
#pragma mark -
@implementation OWSTableSection
+ (OWSTableSection *)sectionWithTitle:(nullable NSString *)title items:(NSArray<OWSTableItem *> *)items
{
OWSTableSection *section = [OWSTableSection new];
section.headerTitle = title;
section.items = [items mutableCopy];
return section;
}
- (instancetype)init
{
if (self = [super init]) {
_items = [NSMutableArray new];
}
return self;
}
- (void)addItem:(OWSTableItem *)item
{
OWSAssertDebug(item);
[_items addObject:item];
}
- (NSUInteger)itemCount
{
return _items.count;
}
@end
#pragma mark -
@interface OWSTableItem ()
@property (nonatomic, nullable) NSString *title;
@property (nonatomic, nullable) OWSTableActionBlock actionBlock;
@property (nonatomic) OWSTableCustomCellBlock customCellBlock;
@property (nonatomic) UITableViewCell *customCell;
@property (nonatomic) NSNumber *customRowHeight;
@end
#pragma mark -
@implementation OWSTableItem
+ (UITableViewCell *)newCell
{
UITableViewCell *cell = [UITableViewCell new];
[self configureCell:cell];
return cell;
}
+ (void)configureCell:(UITableViewCell *)cell
{
cell.backgroundColor = LKColors.cellBackground;
cell.contentView.backgroundColor = UIColor.clearColor;
cell.textLabel.font = [UIFont systemFontOfSize:LKValues.mediumFontSize];
cell.textLabel.textColor = LKColors.text;
cell.detailTextLabel.font = [UIFont systemFontOfSize:LKValues.mediumFontSize];
cell.detailTextLabel.textColor = LKColors.text;
UIView *selectedBackgroundView = [UIView new];
selectedBackgroundView.backgroundColor = LKColors.cellSelected;
cell.selectedBackgroundView = selectedBackgroundView;
}
+ (OWSTableItem *)itemWithTitle:(NSString *)title actionBlock:(nullable OWSTableActionBlock)actionBlock
{
OWSAssertDebug(title.length > 0);
OWSTableItem *item = [OWSTableItem new];
item.actionBlock = actionBlock;
item.title = title;
return item;
}
+ (OWSTableItem *)itemWithCustomCell:(UITableViewCell *)customCell
customRowHeight:(CGFloat)customRowHeight
actionBlock:(nullable OWSTableActionBlock)actionBlock
{
OWSAssertDebug(customCell);
OWSAssertDebug(customRowHeight > 0 || customRowHeight == UITableViewAutomaticDimension);
OWSTableItem *item = [OWSTableItem new];
item.actionBlock = actionBlock;
item.customCell = customCell;
item.customRowHeight = @(customRowHeight);
return item;
}
+ (OWSTableItem *)itemWithCustomCellBlock:(OWSTableCustomCellBlock)customCellBlock
customRowHeight:(CGFloat)customRowHeight
actionBlock:(nullable OWSTableActionBlock)actionBlock
{
OWSAssertDebug(customRowHeight > 0 || customRowHeight == UITableViewAutomaticDimension);
OWSTableItem *item = [self itemWithCustomCellBlock:customCellBlock actionBlock:actionBlock];
item.customRowHeight = @(customRowHeight);
return item;
}
+ (OWSTableItem *)itemWithCustomCellBlock:(OWSTableCustomCellBlock)customCellBlock
actionBlock:(nullable OWSTableActionBlock)actionBlock
{
OWSAssertDebug(customCellBlock);
OWSTableItem *item = [OWSTableItem new];
item.actionBlock = actionBlock;
item.customCellBlock = customCellBlock;
return item;
}
+ (OWSTableItem *)disclosureItemWithText:(NSString *)text actionBlock:(nullable OWSTableActionBlock)actionBlock
{
return [self itemWithText:text actionBlock:actionBlock accessoryType:UITableViewCellAccessoryDisclosureIndicator];
}
+ (OWSTableItem *)disclosureItemWithText:(NSString *)text
accessibilityIdentifier:(nullable NSString *)accessibilityIdentifier
actionBlock:(nullable OWSTableActionBlock)actionBlock
{
return [self itemWithText:text
accessibilityIdentifier:accessibilityIdentifier
actionBlock:actionBlock
accessoryType:UITableViewCellAccessoryNone];
}
+ (OWSTableItem *)checkmarkItemWithText:(NSString *)text actionBlock:(nullable OWSTableActionBlock)actionBlock
{
return [self checkmarkItemWithText:text accessibilityIdentifier:nil actionBlock:actionBlock];
}
+ (OWSTableItem *)checkmarkItemWithText:(NSString *)text
accessibilityIdentifier:(nullable NSString *)accessibilityIdentifier
actionBlock:(nullable OWSTableActionBlock)actionBlock;
{
return [self itemWithText:text
accessibilityIdentifier:accessibilityIdentifier
actionBlock:actionBlock
accessoryType:UITableViewCellAccessoryCheckmark];
}
+ (OWSTableItem *)itemWithText:(NSString *)text
actionBlock:(nullable OWSTableActionBlock)actionBlock
accessoryType:(UITableViewCellAccessoryType)accessoryType
{
return [self itemWithText:text accessibilityIdentifier:nil actionBlock:actionBlock accessoryType:accessoryType];
}
+ (OWSTableItem *)itemWithText:(NSString *)text
accessibilityIdentifier:(nullable NSString *)accessibilityIdentifier
actionBlock:(nullable OWSTableActionBlock)actionBlock
accessoryType:(UITableViewCellAccessoryType)accessoryType
{
OWSAssertDebug(text.length > 0);
OWSAssertDebug(actionBlock);
OWSTableItem *item = [OWSTableItem new];
item.actionBlock = actionBlock;
item.customCellBlock = ^{
UITableViewCell *cell = [OWSTableItem newCell];
cell.textLabel.text = text;
cell.accessoryType = accessoryType;
cell.accessibilityIdentifier = accessibilityIdentifier;
cell.tintColor = LKColors.accent;
return cell;
};
return item;
}
+ (OWSTableItem *)disclosureItemWithText:(NSString *)text
customRowHeight:(CGFloat)customRowHeight
actionBlock:(nullable OWSTableActionBlock)actionBlock
{
return [self disclosureItemWithText:text
accessibilityIdentifier:nil
customRowHeight:customRowHeight
actionBlock:actionBlock];
}
+ (OWSTableItem *)disclosureItemWithText:(NSString *)text
accessibilityIdentifier:(nullable NSString *)accessibilityIdentifier
customRowHeight:(CGFloat)customRowHeight
actionBlock:(nullable OWSTableActionBlock)actionBlock
{
OWSAssertDebug(customRowHeight > 0 || customRowHeight == UITableViewAutomaticDimension);
OWSTableItem *item =
[self disclosureItemWithText:text accessibilityIdentifier:accessibilityIdentifier actionBlock:actionBlock];
item.customRowHeight = @(customRowHeight);
return item;
}
+ (OWSTableItem *)disclosureItemWithText:(NSString *)text
detailText:(NSString *)detailText
actionBlock:(nullable OWSTableActionBlock)actionBlock
{
return [self disclosureItemWithText:text detailText:detailText accessibilityIdentifier:nil actionBlock:actionBlock];
}
+ (OWSTableItem *)disclosureItemWithText:(NSString *)text
detailText:(NSString *)detailText
accessibilityIdentifier:(nullable NSString *)accessibilityIdentifier
actionBlock:(nullable OWSTableActionBlock)actionBlock
{
OWSAssertDebug(text.length > 0);
OWSAssertDebug(actionBlock);
OWSTableItem *item = [OWSTableItem new];
item.actionBlock = actionBlock;
item.customCellBlock = ^{
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1
reuseIdentifier:@"UITableViewCellStyleValue1"];
[OWSTableItem configureCell:cell];
cell.textLabel.text = text;
cell.detailTextLabel.text = detailText;
cell.accessibilityIdentifier = accessibilityIdentifier;
return cell;
};
return item;
}
+ (OWSTableItem *)subPageItemWithText:(NSString *)text actionBlock:(nullable OWSTableSubPageBlock)actionBlock
{
OWSAssertDebug(text.length > 0);
OWSAssertDebug(actionBlock);
OWSTableItem *item = [OWSTableItem new];
__weak OWSTableItem *weakItem = item;
item.actionBlock = ^{
OWSTableItem *strongItem = weakItem;
OWSAssertDebug(strongItem);
OWSAssertDebug(strongItem.tableViewController);
if (actionBlock) {
actionBlock(strongItem.tableViewController);
}
};
item.customCellBlock = ^{
UITableViewCell *cell = [OWSTableItem newCell];
cell.textLabel.text = text;
return cell;
};
return item;
}
+ (OWSTableItem *)subPageItemWithText:(NSString *)text
customRowHeight:(CGFloat)customRowHeight
actionBlock:(nullable OWSTableSubPageBlock)actionBlock
{
OWSAssertDebug(customRowHeight > 0 || customRowHeight == UITableViewAutomaticDimension);
OWSTableItem *item = [self subPageItemWithText:text actionBlock:actionBlock];
item.customRowHeight = @(customRowHeight);
return item;
}
+ (OWSTableItem *)actionItemWithText:(NSString *)text actionBlock:(nullable OWSTableActionBlock)actionBlock
{
return [self actionItemWithText:text accessibilityIdentifier:nil actionBlock:actionBlock];
}
+ (OWSTableItem *)actionItemWithText:(NSString *)text
accessibilityIdentifier:(nullable NSString *)accessibilityIdentifier
actionBlock:(nullable OWSTableActionBlock)actionBlock;
{
OWSAssertDebug(text.length > 0);
OWSAssertDebug(actionBlock);
OWSTableItem *item = [OWSTableItem new];
item.actionBlock = actionBlock;
item.customCellBlock = ^{
UITableViewCell *cell = [OWSTableItem newCell];
cell.textLabel.text = text;
cell.accessibilityIdentifier = accessibilityIdentifier;
return cell;
};
return item;
}
+ (OWSTableItem *)softCenterLabelItemWithText:(NSString *)text
{
OWSAssertDebug(text.length > 0);
OWSTableItem *item = [OWSTableItem new];
item.customCellBlock = ^{
UITableViewCell *cell = [OWSTableItem newCell];
cell.textLabel.text = text;
// These cells look quite different.
//
// Smaller font.
cell.textLabel.font = [UIFont systemFontOfSize:LKValues.mediumFontSize];
// Soft color.
// TODO: Theme, review with design.
cell.textLabel.textColor = LKColors.text;
// Centered.
cell.textLabel.textAlignment = NSTextAlignmentCenter;
cell.userInteractionEnabled = NO;
return cell;
};
return item;
}
+ (OWSTableItem *)softCenterLabelItemWithText:(NSString *)text customRowHeight:(CGFloat)customRowHeight
{
OWSAssertDebug(customRowHeight > 0 || customRowHeight == UITableViewAutomaticDimension);
OWSTableItem *item = [self softCenterLabelItemWithText:text];
item.customRowHeight = @(customRowHeight);
return item;
}
+ (OWSTableItem *)labelItemWithText:(NSString *)text
{
OWSAssertDebug(text.length > 0);
OWSTableItem *item = [OWSTableItem new];
item.customCellBlock = ^{
UITableViewCell *cell = [OWSTableItem newCell];
cell.textLabel.text = text;
cell.userInteractionEnabled = NO;
return cell;
};
return item;
}
+ (OWSTableItem *)labelItemWithText:(NSString *)text accessoryText:(NSString *)accessoryText
{
OWSAssertDebug(text.length > 0);
OWSAssertDebug(accessoryText.length > 0);
OWSTableItem *item = [OWSTableItem new];
item.customCellBlock = ^{
UITableViewCell *cell = [OWSTableItem newCell];
cell.textLabel.text = text;
UILabel *accessoryLabel = [UILabel new];
accessoryLabel.text = accessoryText;
accessoryLabel.textColor = LKColors.text;
accessoryLabel.font = [UIFont systemFontOfSize:LKValues.mediumFontSize];
accessoryLabel.textAlignment = NSTextAlignmentRight;
[accessoryLabel sizeToFit];
cell.accessoryView = accessoryLabel;
cell.userInteractionEnabled = NO;
return cell;
};
return item;
}
+ (OWSTableItem *)longDisclosureItemWithText:(NSString *)text actionBlock:(nullable OWSTableActionBlock)actionBlock
{
OWSAssertDebug(text.length > 0);
OWSTableItem *item = [OWSTableItem new];
item.customCellBlock = ^{
UITableViewCell *cell = [OWSTableItem newCell];
cell.textLabel.text = text;
cell.textLabel.numberOfLines = 0;
cell.textLabel.lineBreakMode = NSLineBreakByWordWrapping;
return cell;
};
item.customRowHeight = @(UITableViewAutomaticDimension);
item.actionBlock = actionBlock;
return item;
}
+ (OWSTableItem *)switchItemWithText:(NSString *)text
isOnBlock:(OWSTableSwitchBlock)isOnBlock
target:(id)target
selector:(SEL)selector
{
return [self switchItemWithText:text
isOnBlock:(OWSTableSwitchBlock)isOnBlock
isEnabledBlock:^{
return YES;
}
target:target
selector:selector];
}
+ (OWSTableItem *)switchItemWithText:(NSString *)text
isOnBlock:(OWSTableSwitchBlock)isOnBlock
isEnabledBlock:(OWSTableSwitchBlock)isEnabledBlock
target:(id)target
selector:(SEL)selector
{
return [self switchItemWithText:text
accessibilityIdentifier:nil
isOnBlock:isOnBlock
isEnabledBlock:isEnabledBlock
target:target
selector:selector];
}
+ (OWSTableItem *)switchItemWithText:(NSString *)text
accessibilityIdentifier:(nullable NSString *)accessibilityIdentifier
isOnBlock:(OWSTableSwitchBlock)isOnBlock
isEnabledBlock:(OWSTableSwitchBlock)isEnabledBlock
target:(id)target
selector:(SEL)selector
{
OWSAssertDebug(text.length > 0);
OWSAssertDebug(target);
OWSAssertDebug(selector);
OWSTableItem *item = [OWSTableItem new];
__weak id weakTarget = target;
item.customCellBlock = ^{
UITableViewCell *cell = [OWSTableItem newCell];
cell.textLabel.text = text;
UISwitch *cellSwitch = [UISwitch new];
cell.accessoryView = cellSwitch;
cellSwitch.onTintColor = LKColors.accent;
[cellSwitch setOn:isOnBlock()];
[cellSwitch addTarget:weakTarget action:selector forControlEvents:UIControlEventValueChanged];
cellSwitch.enabled = isEnabledBlock();
cellSwitch.accessibilityIdentifier = accessibilityIdentifier;
cell.selectionStyle = UITableViewCellSelectionStyleNone;
return cell;
};
return item;
}
- (nullable UITableViewCell *)customCell
{
if (_customCell) {
return _customCell;
}
if (_customCellBlock) {
return _customCellBlock();
}
return nil;
}
@end
#pragma mark -
@interface OWSTableViewController () <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic) UITableView *tableView;
@end
#pragma mark -
NSString *const kOWSTableCellIdentifier = @"kOWSTableCellIdentifier";
@implementation OWSTableViewController
- (instancetype)init
{
self = [super init];
if (!self) {
return self;
}
[self owsTableCommonInit];
return self;
}
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (!self) {
return self;
}
[self owsTableCommonInit];
return self;
}
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (!self) {
return self;
}
[self owsTableCommonInit];
return self;
}
- (void)owsTableCommonInit
{
_contents = [OWSTableContents new];
self.tableViewStyle = UITableViewStyleGrouped;
}
- (void)loadView
{
[super loadView];
OWSAssertDebug(self.contents);
if (self.contents.title.length > 0) {
self.title = self.contents.title;
}
self.tableView = [[UITableView alloc] initWithFrame:CGRectZero style:self.tableViewStyle];
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
[self.view addSubview:self.tableView];
if ([self.tableView applyScrollViewInsetsFix]) {
// if applyScrollViewInsetsFix disables contentInsetAdjustmentBehavior,
// we need to pin to the top and bottom layout guides since UIKit
// won't adjust our content insets.
[self.tableView autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:self.view withOffset:0];
[self.tableView autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.view withOffset:0];
[self.tableView autoPinEdgeToSuperviewSafeArea:ALEdgeLeading];
[self.tableView autoPinEdgeToSuperviewSafeArea:ALEdgeTrailing];
// We don't need a top or bottom insets, since we pin to the top and bottom layout guides.
self.automaticallyAdjustsScrollViewInsets = NO;
} else {
[self.tableView autoPinEdgesToSuperviewEdges];
}
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kOWSTableCellIdentifier];
[self applyTheme];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
}
- (OWSTableSection *)sectionForIndex:(NSInteger)sectionIndex
{
OWSAssertDebug(self.contents);
OWSAssertDebug(sectionIndex >= 0 && sectionIndex < (NSInteger)self.contents.sections.count);
OWSTableSection *section = self.contents.sections[(NSUInteger)sectionIndex];
return section;
}
- (OWSTableItem *)itemForIndexPath:(NSIndexPath *)indexPath
{
OWSAssertDebug(self.contents);
OWSAssertDebug(indexPath.section >= 0 && indexPath.section < (NSInteger)self.contents.sections.count);
OWSTableSection *section = self.contents.sections[(NSUInteger)indexPath.section];
OWSAssertDebug(indexPath.item >= 0 && indexPath.item < (NSInteger)section.items.count);
OWSTableItem *item = section.items[(NSUInteger)indexPath.item];
return item;
}
- (void)setContents:(OWSTableContents *)contents
{
OWSAssertDebug(contents);
OWSAssertIsOnMainThread();
_contents = contents;
[self.tableView reloadData];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
OWSAssertDebug(self.contents);
return (NSInteger)self.contents.sections.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)sectionIndex
{
OWSTableSection *section = [self sectionForIndex:sectionIndex];
OWSAssertDebug(section.items);
return (NSInteger)section.items.count;
}
- (nullable NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)sectionIndex
{
OWSTableSection *section = [self sectionForIndex:sectionIndex];
return section.headerTitle;
}
- (nullable NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)sectionIndex
{
OWSTableSection *section = [self sectionForIndex:sectionIndex];
return section.footerTitle;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
OWSTableItem *item = [self itemForIndexPath:indexPath];
item.tableViewController = self;
UITableViewCell *customCell = [item customCell];
if (customCell) {
return customCell;
}
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kOWSTableCellIdentifier];
OWSAssertDebug(cell);
[OWSTableItem configureCell:cell];
cell.textLabel.text = item.title;
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
OWSTableItem *item = [self itemForIndexPath:indexPath];
if (item.customRowHeight) {
return [item.customRowHeight floatValue];
}
return kOWSTable_DefaultCellHeight;
}
- (nullable UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)sectionIndex
{
OWSTableSection *section = [self sectionForIndex:sectionIndex];
return section.customHeaderView;
}
- (nullable UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)sectionIndex
{
OWSTableSection *section = [self sectionForIndex:sectionIndex];
return section.customFooterView;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)sectionIndex
{
OWSTableSection *_Nullable section = [self sectionForIndex:sectionIndex];
if (!section) {
OWSFailDebug(@"Section index out of bounds.");
return 0;
}
if (section.customHeaderHeight) {
return [section.customHeaderHeight floatValue];
} else if (section.headerTitle.length > 0) {
return UITableViewAutomaticDimension;
} else {
return 0;
}
}
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)sectionIndex
{
OWSTableSection *_Nullable section = [self sectionForIndex:sectionIndex];
if (!section) {
OWSFailDebug(@"Section index out of bounds.");
return 0;
}
if (section.customFooterHeight) {
OWSAssertDebug([section.customFooterHeight floatValue] > 0);
return [section.customFooterHeight floatValue];
} else if (section.footerTitle.length > 0) {
return UITableViewAutomaticDimension;
} else {
return 0;
}
}
// Called before the user changes the selection. Return a new indexPath, or nil, to change the proposed selection.
- (nullable NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
OWSTableItem *item = [self itemForIndexPath:indexPath];
if (!item.actionBlock) {
return nil;
}
return indexPath;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
OWSTableItem *item = [self itemForIndexPath:indexPath];
if (item.actionBlock) {
item.actionBlock();
}
}
#pragma mark Index
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index
{
if (self.contents.sectionForSectionIndexTitleBlock) {
return self.contents.sectionForSectionIndexTitleBlock(title, index);
} else {
return 0;
}
}
- (nullable NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView
{
if (self.contents.sectionIndexTitlesForTableViewBlock) {
return self.contents.sectionIndexTitlesForTableViewBlock();
} else {
return 0;
}
}
#pragma mark - Presentation
- (void)presentFromViewController:(UIViewController *)fromViewController
{
OWSAssertDebug(fromViewController);
OWSNavigationController *navigationController = [[OWSNavigationController alloc] initWithRootViewController:self];
self.navigationItem.leftBarButtonItem =
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemStop
target:self
action:@selector(donePressed:)];
[fromViewController presentViewController:navigationController animated:YES completion:nil];
}
- (void)donePressed:(id)sender
{
[self dismissViewControllerAnimated:YES completion:nil];
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
[self.delegate tableViewWillBeginDragging];
}
- (void)applyTheme
{
OWSAssertIsOnMainThread();
self.view.backgroundColor = LKColors.navigationBarBackground;
self.tableView.backgroundColor = LKColors.navigationBarBackground;
self.tableView.separatorColor = LKColors.separator;
}
@end
NS_ASSUME_NONNULL_END
Loading…
Cancel
Save