Skip to content

Commit

Permalink
Improve demo app UI (#648)
Browse files Browse the repository at this point in the history
  • Loading branch information
pinarol authored Jan 27, 2025
2 parents 60da91e + 73b5589 commit 1c0fd90
Show file tree
Hide file tree
Showing 15 changed files with 739 additions and 435 deletions.
84 changes: 84 additions & 0 deletions Demo/Demo/Gravatar-Demo/Common/BaseFormViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import UIKit
import Combine

struct FormSection: SectionTitle, Hashable {
var sectionTitle: String
}

class FormField: NSObject, @unchecked Sendable {
@MainActor
func dequeueCell(in tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell { fatalError() }
}

class BaseFormViewController: UITableViewController {
@Published var fieldText: String = ""

var cancellables = Set<AnyCancellable>()

let section = FormSection(sectionTitle: "")

var snapshot = NSDiffableDataSourceSnapshot<FormSection, FormField>()
lazy var dataSource: UITableViewDiffableDataSource = SectionTitleTableViewDiffibleDataSource<FormSection, FormField>(tableView: tableView) {
(tableView: UITableView, indexPath: IndexPath, formField: FormField) -> UITableViewCell? in
let cell = formField.dequeueCell(in: tableView, for: indexPath)
cell.backgroundView?.backgroundColor = .secondarySystemBackground
cell.backgroundColor = .secondarySystemBackground
cell.contentView.backgroundColor = .secondarySystemBackground
cell.accessoryView?.backgroundColor = .secondarySystemBackground
return cell
}

var form: [FormField] { [] }

override func viewDidLoad() {
super.viewDidLoad()
tableView.allowsSelection = false
snapshot.appendSections([section])
snapshot.appendItems(form)
dataSource.apply(snapshot)
view.backgroundColor = .secondarySystemBackground
tableView.separatorStyle = .none
tableView.rowHeight = UITableView.automaticDimension
tableView.delaysContentTouches = false
}

func replace(_ oldFormField: FormField, with newFormField: FormField, after: FormField) {
snapshot.deleteItems([oldFormField])
if snapshot.indexOfItem(newFormField) == nil {
snapshot.insertItems([newFormField], afterItem: after)
}
dataSource.apply(snapshot)
}

func update(_ field: FormField, animated: Bool = false) {
update([field])
}

func update(_ fields: [FormField], animated: Bool = false) {
snapshot.reloadItems(fields)
dataSource.apply(snapshot, animatingDifferences: animated)
}


}

class SectionTitleTableViewDiffibleDataSource<SectionType: Hashable, ItemType: Hashable>: UITableViewDiffableDataSource<SectionType, ItemType> where SectionType: SectionTitle, SectionType: Sendable, ItemType: Sendable {

override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return sectionIdentifier(for: section)?.sectionTitle
}
}

protocol SectionTitle {
var sectionTitle: String { get }
}

extension UIControl {
func removeAllActions() {
enumerateEventHandlers { action, _, event, _ in
if let action = action {
removeAction(action, for: event)
}
}
}
}
56 changes: 56 additions & 0 deletions Demo/Demo/Gravatar-Demo/Common/FormFields/ButtonField.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import UIKit

final class ButtonField: FormField, @unchecked Sendable {
var title: String
var isActionButton: Bool
var isEnabled: Bool

private let cellID = "ButtonCell"
private let action: UIAction

@MainActor
init(title: String, isActionButton: Bool = false, enabled: Bool = true, action actionHandler: @escaping UIActionHandler) {
self.title = title
self.isActionButton = isActionButton
self.isEnabled = enabled
self.action = UIAction(handler: actionHandler)
}

@MainActor
override func dequeueCell(in tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as? ButtonCell ?? ButtonCell(reuseIdentifier: cellID)
cell.update(with: self)
cell.button.removeAllActions()
cell.button.addAction(action, for: .touchUpInside)
return cell
}
}

private final class ButtonCell: UITableViewCell {
let button = UIButton()

init(reuseIdentifier: String?) {
super.init(style: .default, reuseIdentifier: reuseIdentifier)
button.translatesAutoresizingMaskIntoConstraints = false

self.contentView.addSubview(button)
NSLayoutConstraint.activate([
contentView.centerXAnchor.constraint(equalTo: button.centerXAnchor),
contentView.topAnchor.constraint(equalTo: button.topAnchor),
contentView.bottomAnchor.constraint(equalTo: button.bottomAnchor, constant: 8),
])
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

func update(with field: ButtonField) {
var config = field.isActionButton ? UIButton.Configuration.borderedProminent() : .plain()
config.title = field.title
button.configuration = config
button.isEnabled = field.isEnabled
button.sizeToFit()
}
}

56 changes: 56 additions & 0 deletions Demo/Demo/Gravatar-Demo/Common/FormFields/ButtonLabelField.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import UIKit

final class ButtonLabelField: FormField, @unchecked Sendable {
var buttonTitle: String
var title: String
var subtitle: String?
var isEnabled: Bool

private let cellID = "ButtonCellCell"
private let action: UIAction

@MainActor
init(title: String, subtitle: String?, buttonTitle: String, isEnabled: Bool = true, action actionHandler: @escaping UIActionHandler) {
self.title = title
self.subtitle = subtitle
self.buttonTitle = buttonTitle
self.isEnabled = isEnabled
self.action = UIAction(handler: actionHandler)
}

@MainActor
override func dequeueCell(in tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as? ButtonLabelCell ?? ButtonLabelCell(reuseIdentifier: cellID)
cell.update(with: self)
cell.button.removeAllActions()
cell.button.addAction(action, for: .touchUpInside)
return cell
}
}

private final class ButtonLabelCell: UITableViewCell {
let button = UIButton()

init(reuseIdentifier: String?) {
super.init(style: .default, reuseIdentifier: reuseIdentifier)
accessoryView = button
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

func update(with field: ButtonLabelField) {
var config = UIButton.Configuration.plain()
config.title = field.buttonTitle
button.configuration = config
button.sizeToFit()
button.isEnabled = field.isEnabled

var cellConfig = self.defaultContentConfiguration()
cellConfig.text = field.title
cellConfig.secondaryText = field.subtitle

self.contentConfiguration = cellConfig
}
}
62 changes: 62 additions & 0 deletions Demo/Demo/Gravatar-Demo/Common/FormFields/ImageFormField.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import UIKit
import Combine

class ImageFormField: FormField, @unchecked Sendable, UITextFieldDelegate {
var image: UIImage?
var size: CGSize
private(set) var imageView: UIImageView?
private var cancellables = Set<AnyCancellable>()

private let cellID = "ImageFormCell"

init(image: UIImage? = nil, size: CGSize) {
self.image = image
self.size = size
}

@MainActor
override func dequeueCell(in tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as? ImageCell ?? ImageCell(reuseIdentifier: cellID)
cell.update(with: self)
imageView = cell.formImageView
cell.formImageView.publisher(for: \.image).sink { [weak self] image in
self?.image = image
}.store(in: &cancellables)

return cell
}
}

private final class ImageCell: UITableViewCell {
let formImageView = UIImageView()

private lazy var widthConstraint: NSLayoutConstraint = formImageView.widthAnchor.constraint(equalToConstant: 300)
private lazy var heightConstraint: NSLayoutConstraint = formImageView.heightAnchor.constraint(equalToConstant: 300)

init(reuseIdentifier: String?) {
super.init(style: .default, reuseIdentifier: reuseIdentifier)
formImageView.translatesAutoresizingMaskIntoConstraints = false
formImageView.backgroundColor = .tertiarySystemBackground

self.contentView.addSubview(formImageView)
NSLayoutConstraint.activate([
contentView.centerXAnchor.constraint(equalTo: formImageView.centerXAnchor),
contentView.topAnchor.constraint(equalTo: formImageView.topAnchor),
contentView.bottomAnchor.constraint(equalTo: formImageView.bottomAnchor, constant: 8),
widthConstraint,
heightConstraint
])
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

func update(with config: ImageFormField) {
widthConstraint.constant = config.size.width
heightConstraint.constant = config.size.height
if let image = config.image {
formImageView.image = image
}
}
}
24 changes: 24 additions & 0 deletions Demo/Demo/Gravatar-Demo/Common/FormFields/LabelField.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import UIKit

final class LabelField: FormField, @unchecked Sendable {
var title: String?
var subtitle: String?
private let cellID = "LabelCell"

init(title: String? = nil, subtitle: String? = nil) {
self.title = title
self.subtitle = subtitle
}

@MainActor
override func dequeueCell(in tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellID) ?? UITableViewCell(style: .subtitle, reuseIdentifier: cellID)

var config = cell.defaultContentConfiguration()
config.text = title
config.secondaryText = subtitle
cell.contentConfiguration = config

return cell
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import UIKit

class SegmentedControlField: FormField, @unchecked Sendable, UITextFieldDelegate {
typealias OnSegmentSelected = (String, Int) -> Void

var segments: [String]
let actionHandler: OnSegmentSelected?

@Published var selectedIndex: Int
@Published var selectedSegment: String = ""

private let cellID = "ImageFormCell"

init(segments: [String], selectedIndex: Int = 0, action actionhandler: OnSegmentSelected? = nil) {
self.segments = segments
self.selectedIndex = selectedIndex
self.actionHandler = actionhandler
if segments.indices.contains(selectedIndex) {
selectedSegment = segments[selectedIndex]
}
}

@MainActor
override func dequeueCell(in tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as? SegmentedControlCell ?? SegmentedControlCell(reuseIdentifier: cellID)
cell.update(with: self)
cell.selector.addAction(UIAction { [weak self] _ in
guard let self else { return }
selectedIndex = cell.selector.selectedSegmentIndex
selectedSegment = segments[selectedIndex]
actionHandler?(selectedSegment, selectedIndex)
}, for: .valueChanged)
return cell
}
}

private final class SegmentedControlCell: UITableViewCell {
let selector = UISegmentedControl()

init(reuseIdentifier: String?) {
super.init(style: .default, reuseIdentifier: reuseIdentifier)
selector.translatesAutoresizingMaskIntoConstraints = false

self.contentView.addSubview(selector)
NSLayoutConstraint.activate([
contentView.centerXAnchor.constraint(equalTo: selector.centerXAnchor),
contentView.topAnchor.constraint(equalTo: selector.topAnchor),
contentView.bottomAnchor.constraint(equalTo: selector.bottomAnchor, constant: 8),
])
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

func update(with config: SegmentedControlField) {
selector.removeAllSegments()
config.segments.enumerated().forEach {
selector.insertSegment(withTitle: $1, at: $0, animated: true)
}
selector.selectedSegmentIndex = config.selectedIndex
}
}
Loading

0 comments on commit 1c0fd90

Please sign in to comment.