Initial commit

This commit is contained in:
Michael Ong
2023-02-03 15:53:50 +08:00
parent 1954bfde59
commit e4b691faef
37 changed files with 1677 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
.swiftpm
.vscode

5
.swiftformat Normal file
View File

@@ -0,0 +1,5 @@
--swiftversion 5.7
--indent 4
--maxwidth 120
--wrapparameters before-first
--header "{file}\n\nCode Copyright Buslo Collective\nCreated {created}"

23
Package.resolved Normal file
View File

@@ -0,0 +1,23 @@
{
"pins" : [
{
"identity" : "snapkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/snapkit/snapkit.git",
"state" : {
"revision" : "f222cbdf325885926566172f6f5f06af95473158",
"version" : "5.6.0"
}
},
{
"identity" : "swiftformat",
"kind" : "remoteSourceControl",
"location" : "https://github.com/nicklockwood/swiftformat",
"state" : {
"revision" : "ae4d0b672cd7094ae513097c617951a5ac714418",
"version" : "0.50.8"
}
}
],
"version" : 2
}

26
Package.swift Normal file
View File

@@ -0,0 +1,26 @@
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "makata",
platforms: [.iOS(.v15), .macCatalyst(.v15)],
products: [
.library(name: "makata", targets: ["makataInteraction", "makataForm", "makataUserInterface"]),
],
dependencies: [
.package(url: "https://github.com/snapkit/snapkit.git", .upToNextMajor(from: "5.0.1")),
.package(url: "https://github.com/nicklockwood/swiftformat", from: "0.50.4"),
],
targets: [
.target(name: "makataUserInterface",
dependencies: ["makataForm", "makataInteraction", .product(name: "SnapKit", package: "snapkit")],
path: "Sources/user-interface"),
.target(name: "makataForm",
dependencies: ["makataInteraction"],
path: "Sources/form"),
.target(name: "makataInteraction",
path: "Sources/interaction"),
]
)

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# makata
Foundations for writing iOS apps in the least amount of time

View File

@@ -0,0 +1,32 @@
//
// File.swift
//
//
// Created by Michael Ong on 2/3/23.
//
import Foundation
public enum FieldError: LocalizedError {
case required
case wrongFormat
case invalid(String)
case incomplete(Error?)
public var errorDescription: String? {
switch self {
case .required:
return "is required"
case .wrongFormat:
return "is in a wrong format"
case let .incomplete(error):
if let error {
return "is incomplete, \(error.localizedDescription)"
} else {
return "is incomplete"
}
case let .invalid(string):
return string
}
}
}

View File

@@ -0,0 +1,41 @@
//
// File.swift
//
//
// Created by Michael Ong on 2/3/23.
//
import Foundation
public enum FieldPartialValue<Complete, Incomplete> {
case complete(Complete)
case partial(Incomplete, Error?)
}
@dynamicMemberLookup
public struct EnsureCompleteFields<Shape> {
var shape: Shape
public init(checking shape: Shape) {
self.shape = shape
}
public subscript<Value>(dynamicMember member: KeyPath<Shape, FieldPartialValue<Value, some Any>>) -> Value {
get throws {
switch shape[keyPath: member] {
case let .partial(_, error):
if let error {
throw error
} else {
fatalError()
}
case let .complete(value):
return value
}
}
}
public subscript<Value>(dynamicMember member: KeyPath<Shape, Value>) -> Value {
shape[keyPath: member]
}
}

View File

@@ -0,0 +1,16 @@
//
// File.swift
//
//
// Created by Michael Ong on 2/3/23.
//
import Foundation
public struct FieldValidator<Value> {
public let validate: (Value) throws -> Void
public init(validate: @escaping (Value) throws -> Void) {
self.validate = validate
}
}

View File

@@ -0,0 +1,153 @@
//
// File.swift
//
//
// Created by Michael Ong on 2/3/23.
//
import Foundation
public struct Binding<Source: AnyObject, Value> {
public let action: (Value) throws -> Value
public let initialValue: Value?
public init(
source: Source,
to path: ReferenceWritableKeyPath<Source, Value>
) {
action = { [unowned source] value in
source[keyPath: path] = value
return value
}
initialValue = source[keyPath: path]
}
public init(
source: Source,
to path: ReferenceWritableKeyPath<Source, Value?>
) {
action = { [unowned source] value in
source[keyPath: path] = value
return value
}
initialValue = source[keyPath: path]
}
public init<Out, T: FieldTransformable>(
source: Source,
to path: ReferenceWritableKeyPath<Source, Out>,
transform: T
) throws where T.Value == Value, T.Output == Out {
action = { [unowned source] value in
source[keyPath: path] = try transform.encode(to: value)
return value
}
initialValue = try transform.decode(from: source[keyPath: path])
}
public init<F: FieldFormattable>(
source: Source,
to path: ReferenceWritableKeyPath<Source, Value>,
format: F
) where F.Input == Value, F.Output == Value {
action = { [unowned source] value in
source[keyPath: path] = value
return format.format(value: value)
}
initialValue = source[keyPath: path]
}
public init<Out, T: FieldTransformable, F: FieldFormattable>(
source: Source,
to path: ReferenceWritableKeyPath<Source, Out>,
transform: T,
format: F
) throws where T.Value == Value, T.Output == Out, F.Input == Out, F.Output == Value {
action = { [unowned source] value in
let rawValue = try transform.encode(to: value)
source[keyPath: path] = rawValue
return format.format(value: rawValue)
}
initialValue = try transform.decode(from: source[keyPath: path])
}
}
public extension Binding {
init<Complete, T: FieldTransformable>(
source: Source,
to path: FieldPartialValueKeyPath<Source, Complete, Value>,
transform: T
) where T.Output == Complete, T.Value == Value {
action = { [unowned source] value in
do {
source[keyPath: path] = .complete(try transform.encode(to: value))
} catch {
source[keyPath: path] = .partial(value, error)
}
return value
}
do {
switch source[keyPath: path] {
case let .complete(complete):
initialValue = try transform.decode(from: complete)
case let .partial(partial, _):
initialValue = partial
}
} catch {
fatalError("Are you intentionally setting incorrect data?")
}
}
init<Complete, T: FieldTransformable, F: FieldFormattable>(
source: Source,
to path: FieldPartialValueKeyPath<Source, Complete, Value>,
transform: T,
format: F
) where T.Value == Value, T.Output == Complete, F.Input == Complete, F.Output == Value {
action = { [unowned source] value in
let partialResult: FieldPartialValue<Complete, Value>
do {
partialResult = .complete(try transform.encode(to: value))
} catch {
partialResult = .partial(value, error)
}
source[keyPath: path] = partialResult
switch partialResult {
case let .complete(complete):
return format.format(value: complete)
case let .partial(incomplete, _):
return incomplete
}
}
do {
switch source[keyPath: path] {
case let .complete(complete):
initialValue = try transform.decode(from: complete)
case let .partial(partial, _):
initialValue = partial
}
} catch {
fatalError("Are you intentionally setting incorrect data?")
}
}
}
public typealias FieldPartialValueKeyPath<Source, Complete, Value> = ReferenceWritableKeyPath<
Source, FieldPartialValue<Complete, Value>
>

View File

@@ -0,0 +1,146 @@
//
// File.swift
//
//
// Created by Michael Ong on 2/3/23.
//
import Foundation
public struct FormValidation<Shape> {
public struct Result {
public static var noErrors: Result { Result(fields: [:]) }
public let fields: [PartialKeyPath<Shape>: [Error]]
}
let validationsDict: [PartialKeyPath<Shape>: (Shape) -> [Error]]
public init() {
validationsDict = [:]
}
init(_ newDict: [PartialKeyPath<Shape>: (Shape) -> [Error]]) {
validationsDict = newDict
}
public func validations<Value>(
for path: KeyPath<Shape, Value>,
are fields: FieldValidator<Value>...
) -> Self {
var newDict = validationsDict
newDict[path] = { shape in
var fieldErrors = [Error]()
fields.forEach { field in
do {
try field.validate(shape[keyPath: path])
} catch {
fieldErrors.append(error)
}
}
return fieldErrors
}
return .init(newDict)
}
public func validations<Value>(
for path: KeyPath<Shape, Value?>,
are fields: FieldValidator<Value>...
) -> Self {
var newDict = validationsDict
newDict[path] = { shape in
var fieldErrors = [Error]()
fields.forEach { field in
do {
if let value = shape[keyPath: path] {
try field.validate(value)
} else {
fatalError("Tried to validate a field that does not exist or is nil.")
}
} catch {
fieldErrors.append(error)
}
}
return fieldErrors
}
return .init(newDict)
}
public func validations<Value>(
for path: KeyPath<Shape, FieldPartialValue<Value, some Any>>,
are fields: FieldValidator<Value>...
) -> Self {
var newDict = validationsDict
newDict[path] = { shape in
var fieldErrors = [Error]()
fields.forEach { field in
do {
switch shape[keyPath: path] {
case let .complete(complete):
try field.validate(complete)
case let .partial(_, error):
throw FieldError.incomplete(error)
}
} catch {
fieldErrors.append(error)
}
}
return fieldErrors
}
return .init(newDict)
}
public func validations<Value>(
for path: KeyPath<Shape, FieldPartialValue<Value, some Any>?>,
are fields: FieldValidator<Value>...
) -> Self {
var newDict = validationsDict
newDict[path] = { shape in
var fieldErrors = [Error]()
fields.forEach { field in
do {
if let value = shape[keyPath: path] {
switch value {
case let .complete(complete):
try field.validate(complete)
case let .partial(_, error):
throw FieldError.incomplete(error)
}
} else {
fatalError("Tried to validate a field that does not exist or is nil.")
}
} catch {
fieldErrors.append(error)
}
}
return fieldErrors
}
return .init(newDict)
}
public func validate(_ shape: Shape) -> Result {
let errors = validationsDict.compactMap { key, value in
let fieldErrors = value(shape)
if !fieldErrors.isEmpty {
return (key, fieldErrors)
} else {
return nil
}
}
return .init(fields: .init(uniqueKeysWithValues: errors))
}
}

8
Sources/form/form.swift Normal file
View File

@@ -0,0 +1,8 @@
//
// File.swift
//
//
// Created by Michael Ong on 2/3/23.
//
import Foundation

View File

@@ -0,0 +1,15 @@
//
// File.swift
//
//
// Created by Michael Ong on 2/3/23.
//
import Foundation
public protocol FieldFormattable {
associatedtype Input
associatedtype Output
func format(value: Input) -> Output
}

View File

@@ -0,0 +1,16 @@
//
// File.swift
//
//
// Created by Michael Ong on 2/3/23.
//
import Foundation
public protocol FieldTransformable {
associatedtype Value
associatedtype Output
func encode(to value: Value) throws -> Output
func decode(from value: Output) throws -> Value
}

View File

@@ -0,0 +1,97 @@
//
// File.swift
//
//
// Created by Michael Ong on 2/3/23.
//
import Foundation
public protocol Formable: AnyObject {
associatedtype FormData
var formHandler: FormHandler<FormData> { get }
func submitData(form: FormData) async throws
}
public extension Formable {
func submit() async throws {
try await formHandler.submit { [unowned self] shape in
try await submitData(form: shape)
}
}
}
@dynamicMemberLookup
public class FormHandler<Shape> {
public enum Errors: Error {
case invalid(FormValidation<Shape>.Result)
}
public struct State {
public let isValid: Bool
public let validationResult: FormValidation<Shape>.Result
}
public typealias UpdatesHandler = (Shape, State) async -> Void
public var current: Shape
var updateHandler: UpdatesHandler = { _, _ async in }
var validations: FormValidation<Shape>?
public init(initial: Shape) {
self.current = initial
}
func submit(_ callback: @escaping (Shape) async throws -> Void) async throws {
let errors = validations?.validate(current) ?? .noErrors
if !errors.fields.isEmpty {
throw Errors.invalid(errors)
}
try await callback(current)
}
func pushUpdates() {
Task {
let result = validations?.validate(current) ?? .noErrors
await updateHandler(current, .init(isValid: result.fields.isEmpty, validationResult: result))
}
}
}
extension FormHandler {
@discardableResult
public func callAsFunction(_ callback: @escaping UpdatesHandler) -> Self {
updateHandler = callback
pushUpdates()
return self
}
@discardableResult
public func setValidationHandler(_ handler: FormValidation<Shape>?) -> Self {
validations = handler
pushUpdates()
return self
}
}
extension FormHandler {
public subscript<Value>(dynamicMember member: WritableKeyPath<Shape, Value>) -> Value {
get {
current[keyPath: member]
}
set {
current[keyPath: member] = newValue
pushUpdates()
}
}
}

View File

@@ -0,0 +1,6 @@
// interaction.swift
//
// Code Copyright Buslo Collective
// Created 2/3/23
import Foundation

View File

@@ -0,0 +1,16 @@
// protocol-assignable.swift
//
// Code Copyright Buslo Collective
// Created 2/3/23
import Foundation
public protocol Assignable: AnyObject {}
public extension Assignable {
func assign(to target: inout Self?) -> Self {
target = self
return self
}
}

View File

@@ -0,0 +1,22 @@
// protocol-hookable+routeable.swift
//
// Code Copyright Buslo Collective
// Created 2/3/23
import Foundation
public extension Hookable where Hook: Routeable {
init(hook: Hook, routeHandler: @MainActor @escaping (Hook.Route) async -> Void) {
self.init(hook: hook)
hook.routeHandler(routeHandler)
}
init(hook: Hook, routeHandler: @MainActor @escaping (Self, Hook.Route) async -> Void) {
self.init(hook: hook)
hook.routeHandler { [unowned self] route in
await routeHandler(self, route)
}
}
}

View File

@@ -0,0 +1,14 @@
// protocol-hookable.swift
//
// Code Copyright Buslo Collective
// Created 2/3/23
import Foundation
public protocol Hookable: AnyObject {
associatedtype Hook
var client: Hook { get }
init(hook: Hook)
}

View File

@@ -0,0 +1,10 @@
// protocol-lifetimeable.swift
//
// Code Copyright Buslo Collective
// Created 2/3/23
import Foundation
public protocol Lifetimeable: AnyObject {
func cancel()
}

View File

@@ -0,0 +1,42 @@
// protocol-loadable.swift
//
// Code Copyright Buslo Collective
// Created 2/3/23
import Foundation
public protocol Loadable: Stateable where State == LoadableDataState<LoadableData> {
associatedtype LoadableData
func loadData() async throws -> LoadableData
func invalidate()
}
public extension Loadable {
func invalidate() async {
let previousData: LoadableData?
if case let .success(pd) = stateHandler.current {
previousData = pd
} else {
previousData = nil
}
await updateState(to: .pending(previousData))
do {
let newData = try await loadData()
await updateState(to: .success(newData))
} catch {
await updateState(to: .failure(previousData, error))
}
}
}
public enum LoadableDataState<Data> {
case success(Data)
case pending(Data?)
case failure(Data?, Error)
}

View File

@@ -0,0 +1,90 @@
// protocol-observable.swift
//
// Code Copyright Buslo Collective
// Created 2/3/23
import Foundation
@propertyWrapper
public final class Observable<Value> {
public var wrappedValue: Value {
didSet {
subscriptions.forEach { item in
item.action(wrappedValue)
}
}
}
public var projectedValue: Projection {
.init(observable: self)
}
var subscriptions: [Subscription] = []
public init(wrappedValue: Value) {
self.wrappedValue = wrappedValue
}
}
public extension Observable {
class Subscription: Lifetimeable {
public static func equal(source: Subscription) -> (Subscription) -> Bool {
{ check in check.id == source.id }
}
let id: Int
let source: String
let action: (Value) -> Void
weak var observable: Observable<Value>!
init(id: Int, source: String, action: @escaping (Value) -> Void) {
self.id = id
self.source = source
self.action = action
}
deinit {
cancel()
}
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
public func cancel() {
observable.subscriptions.removeAll(where: Subscription.equal(source: self))
}
}
struct Projection {
weak var observable: Observable<Value>!
public func subscribe(
source: @autoclosure () -> String = "[\(#file)]@\(#line) > \(#filePath)",
action: @escaping (Value) -> Void
) -> Lifetimeable {
let newSubscription = Subscription(id: observable.subscriptions.count, source: source(), action: action)
newSubscription.observable = observable
observable.subscriptions.append(newSubscription)
return newSubscription
}
public func bind(
source: @autoclosure () -> String = "[\(#file)]@\(#line) > \(#filePath)",
to loadable: some Loadable
) -> Lifetimeable {
subscribe(source: source()) { [weak loadable] _ in
guard let loadable else {
return
}
Task {
await loadable.invalidate()
}
}
}
}
}

View File

@@ -0,0 +1,30 @@
// protocol-routeable.swift
//
// Code Copyright Buslo Collective
// Created 2/3/23
import Foundation
public protocol Routeable: AnyObject {
associatedtype Route
var routeHandler: RouteCallback<Route> { get set }
}
public extension Routeable {
func updateRoute(to route: Route) async {
await routeHandler.provider(route)
}
}
public class RouteCallback<Route> {
public typealias Provider = @MainActor (Route) async -> Void
var provider: Provider = { _ in }
public init() {}
public func callAsFunction(_ callback: @escaping Provider) {
provider = callback
}
}

View File

@@ -0,0 +1,38 @@
// protocol-stateable.swift
//
// Code Copyright Buslo Collective
// Created 2/3/23
import Foundation
public protocol Stateable: AnyObject {
associatedtype State
var stateHandler: StateHandler<State> { get set }
}
public extension Stateable {
func updateState(to state: State) async {
await stateHandler.provider(state)
}
}
public class StateHandler<State> {
public typealias Provider = @MainActor (State) async -> Void
var provider: Provider = { _ in }
public var current: State
public init(initial: State) {
current = initial
}
public func callAsFunction(_ callback: @escaping Provider) {
provider = callback
Task {
await callback(current)
}
}
}

View File

@@ -0,0 +1,22 @@
// protocol-withable.swift
//
// Code Copyright Buslo Collective
// Created 2/3/23
import Foundation
public protocol Withable {}
public extension Withable {
func with(_ closure: (Self) -> Void) -> Self {
closure(self)
return self
}
func with<Value>(path: KeyPath<Self, Value>, _ closure: (Value) -> Void) -> Self {
closure(self[keyPath: path])
return self
}
}

View File

@@ -0,0 +1,346 @@
// controller-template-collection.swift
//
// Code Copyright Buslo Collective
// Created 2/3/23
import Foundation
import UIKit
public protocol CollectionDelegate: AnyObject {
associatedtype DelegateItemType: Hashable
@MainActor func collectionItemSelected(at indexPath: IndexPath, _ item: DelegateItemType) async
}
public extension Templates {
final class Collection<S: Hashable, E: Hashable>: UIView {
public typealias SectionLayout = (DataSource, Int, S) -> NSCollectionLayoutSection
public class DataSource: UICollectionViewDiffableDataSource<S, E> {}
public let dataSource: DataSource
public private(set) weak var collectionView: UICollectionView!
let delegate = DelegateProxy<E>()
public init(
header: UIView? = nil,
footer: UIView? = nil,
source: (UICollectionView) -> DataSource,
layout: @escaping SectionLayout
) {
let headerRegistration = Header.Registration
let footerRegistration = Footer.Registration
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: .init())
collectionView.contentInset = .init(top: 0, left: 0, bottom: 8, right: 0)
collectionView.alwaysBounceVertical = false
collectionView.delegate = delegate
collectionView.backgroundColor = .systemBackground
let source = source(collectionView)
dataSource = source
super.init(frame: .zero)
let initialSupplementaryProvider = source.supplementaryViewProvider
source.supplementaryViewProvider = { cv, ek, ip in
switch ek {
case CollectionHeaderElementType:
return cv
.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: ip)
.setContainingView(header!)
case CollectionFooterElementType:
return cv
.dequeueConfiguredReusableSupplementary(using: footerRegistration, for: ip)
.setContainingView(footer!)
default:
if let initialSupplementaryProvider {
return initialSupplementaryProvider(cv, ek, ip)
} else {
fatalError("Called for a supplementary view, but no provider.")
}
}
}
addSubview(view: collectionView) { make in
make.edges
.equalToSuperview()
}
let configuration = UICollectionViewCompositionalLayoutConfiguration()
configuration.contentInsetsReference = .layoutMargins
if header != nil {
let headerSupplementaryItem = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: .init(
widthDimension: .fractionalWidth(1),
heightDimension: .estimated(120)
),
elementKind: CollectionHeaderElementType,
alignment: .top
)
headerSupplementaryItem.pinToVisibleBounds = true
configuration.boundarySupplementaryItems.append(headerSupplementaryItem)
}
if footer != nil {
let footerSupplementaryItem = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: .init(
widthDimension: .fractionalWidth(1),
heightDimension: .estimated(30)
),
elementKind: CollectionFooterElementType,
alignment: .bottom
)
footerSupplementaryItem.pinToVisibleBounds = true
configuration.boundarySupplementaryItems.append(footerSupplementaryItem)
}
collectionView.setCollectionViewLayout(
UICollectionViewCompositionalLayout(sectionProvider: { section, _ in
guard let sectionKind = source.sectionIdentifier(for: section) else {
fatalError()
}
return layout(source, section, sectionKind)
}, configuration: configuration),
animated: false
)
self.collectionView = collectionView
}
@available(*, unavailable)
public required init?(coder _: NSCoder) {
fatalError()
}
public func setCollectionDelegate<D: CollectionDelegate>(_ target: D) where D.DelegateItemType == E {
delegate.setupDelegate(delegate: target, dataSource: dataSource)
}
public func updateLayout(action: (LayoutUpdateContext) -> Void) {
action(.init(source: self))
collectionView.collectionViewLayout.invalidateLayout()
}
public func setRefresh(action: @escaping @MainActor () async -> Void) {
#if !targetEnvironment(macCatalyst)
let refreshControl = UIRefreshControl(frame: .zero)
refreshControl.layer.zPosition = 10000
refreshControl.addAction(UIAction { [unowned refreshControl] _ in
Task {
await action()
refreshControl.endRefreshing()
}
}, for: .primaryActionTriggered)
collectionView.refreshControl = refreshControl
#endif
}
public func createSnapsot() -> NSDiffableDataSourceSnapshot<S, E> {
.init()
}
}
}
extension Templates.Collection {
public struct LayoutUpdateContext {
weak var source: Templates.Collection<S, E>!
public func cell<View>(
for item: E,
dequeuedAs _: View.Type
) -> View? where View: UICollectionViewCell {
guard let formIndexPath = source.dataSource.indexPath(for: item) else {
return nil
}
return source.collectionView.cellForItem(at: formIndexPath) as? View
}
}
class Header: UICollectionReusableView {
static var Registration: UICollectionView.SupplementaryRegistration<Header> {
.init(elementKind: CollectionHeaderElementType) { supplementaryView, _, _ in
supplementaryView.subviews.forEach { $0.removeFromSuperview() }
supplementaryView.layer.zPosition = 1000
}
}
weak var border: UIView!
weak var visualEffect: UIVisualEffectView!
weak var keyvalueObx: NSKeyValueObservation?
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
switch traitCollection.userInterfaceStyle {
case .dark:
_ = visualEffect
.contentView
.backgroundColor(.clear)
default:
_ = visualEffect
.contentView
.backgroundColor(.white.withAlphaComponent(0.65))
}
super.traitCollectionDidChange(previousTraitCollection)
}
func setContainingView(_ content: UIView) -> Self {
addSubview(
view: UIVisualEffectView(
effect: UIBlurEffect(style: .regular)
)
.assign(to: &visualEffect)
.hidden()
) { make in
make.leading
.bottom
.trailing
.equalToSuperview()
make.top
.equalToSuperview()
.inset(-300)
}
addSubview(view: content) { make in
make.edges
.equalToSuperview()
}
addSubview(
view: UIView()
.backgroundColor(.separator)
.hidden()
.assign(to: &border)
) { make in
make.horizontalEdges
.bottom
.equalToSuperview()
make.height
.equalTo(1 / UIScreen.main.scale)
}
traitCollectionDidChange(nil)
return self
}
func displayUpdate(_ showBorder: Bool) {
layer.shadowOffset = .init(width: 0, height: 1 / UIScreen.main.scale)
layer.shadowColor = UIColor.black.cgColor
border.isHidden = !showBorder
visualEffect.isHidden = !showBorder
}
}
class Footer: UICollectionReusableView {
static var Registration: UICollectionView.SupplementaryRegistration<Footer> {
.init(elementKind: CollectionFooterElementType) { supplementaryView, _, _ in
supplementaryView.subviews.forEach { $0.removeFromSuperview() }
supplementaryView.layer.zPosition = 1000
}
}
weak var visualEffect: UIVisualEffectView!
var keyvalueObx: NSKeyValueObservation?
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
switch traitCollection.userInterfaceStyle {
case .dark:
_ = visualEffect
.contentView
.backgroundColor(.clear)
default:
_ = visualEffect
.contentView
.backgroundColor(.white.withAlphaComponent(0.65))
}
super.traitCollectionDidChange(previousTraitCollection)
}
func setContainingView(_ content: UIView) -> Self {
addSubview(
view: UIVisualEffectView(
effect: UIBlurEffect(style: .regular)
)
.assign(to: &visualEffect)
.hidden()
) { make in
make.leading
.top
.trailing
.equalToSuperview()
make.bottom
.equalToSuperview()
.inset(-300)
}
addSubview(view: content) { make in
make.edges
.equalToSuperview()
}
traitCollectionDidChange(nil)
return self
}
}
}
extension Templates.Collection {
class DelegateProxy<E: Hashable>: NSObject, UICollectionViewDelegate {
typealias DataSource<Section: Hashable> = UICollectionViewDiffableDataSource<Section, E>
var itemSelected: (IndexPath) -> Void = { _ in }
func setupDelegate<D: CollectionDelegate>(
delegate: D,
dataSource: DataSource<some Hashable>
) where E == D.DelegateItemType {
itemSelected = { [unowned delegate, unowned dataSource] ip in
Task(priority: .high) { @MainActor [unowned delegate, unowned dataSource] () in
await delegate.collectionItemSelected(at: ip, dataSource.itemIdentifier(for: ip)!)
}
}
}
func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) {
itemSelected(indexPath)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let ypos = scrollView.contentOffset.y + scrollView.safeAreaInsets.top
if let collectionView = scrollView as? UICollectionView {
for view in collectionView.visibleSupplementaryViews(ofKind: CollectionHeaderElementType) {
guard let header = view as? Header else {
continue
}
header.displayUpdate(ypos >= 1)
}
}
}
}
}
let CollectionHeaderElementType = "guni__header"
let CollectionFooterElementType = "guni__footer"

View File

@@ -0,0 +1,15 @@
//
// File.swift
//
//
// Created by Michael Ong on 2/3/23.
//
import Foundation
import UIKit
public extension Templates {
final class Form: UIScrollView {
}
}

View File

@@ -0,0 +1,10 @@
//
// File.swift
//
//
// Created by Michael Ong on 2/3/23.
//
import Foundation
public enum Templates {}

View File

@@ -0,0 +1,19 @@
// controller-templated.swift
//
// Code Copyright Buslo Collective
// Created 2/3/23
import Foundation
import UIKit
open class ControllerTemplated<Template: UIView, Hook>: Controller<Hook> {
public lazy var screenTemplate: Template = loadTemplate()
open func loadTemplate() -> Template {
fatalError()
}
override open func loadView() {
view = screenTemplate
}
}

View File

@@ -0,0 +1,37 @@
// controller.swift
//
// Code Copyright Buslo Collective
// Created 2/3/23
import Foundation
import makataInteraction
import UIKit
open class Controller<Hook>: UIViewController, Hookable {
public var client: Hook
public required init(hook: Hook) {
client = hook
super.init(nibName: nil, bundle: nil)
configureController()
}
@available(*, unavailable)
public required init?(coder _: NSCoder) {
fatalError()
}
override public func viewDidLoad() {
super.viewDidLoad()
if let loadable = client as? (any Loadable) {
Task {
await loadable.invalidate()
}
}
}
open func configureController() {}
}

View File

@@ -0,0 +1,16 @@
// protocol-attributable.swift
//
// Code Copyright Buslo Collective
// Created 2/3/23
import Foundation
public protocol Attributable: AnyObject {}
public extension Attributable {
func attribute<Value>(on: ReferenceWritableKeyPath<Self, Value>, _ value: Value) -> Self {
self[keyPath: on] = value
return self
}
}

View File

@@ -0,0 +1,12 @@
//
// File.swift
//
//
// Created by Michael Ong on 2/3/23.
//
import Foundation
public protocol ButtonLoadable: AnyObject {
var isLoading: Bool { get set }
}

View File

@@ -0,0 +1,12 @@
//
// File.swift
//
//
// Created by Michael Ong on 2/3/23.
//
import Foundation
public protocol FailureCallable: AnyObject {
func showFailure(_ error: Error)
}

View File

@@ -0,0 +1,9 @@
// user-interface.swift
//
// Code Copyright Buslo Collective
// Created 2/3/23
import Foundation
public typealias VoidCallbackAsync = () async throws -> Void
public typealias ErrorCallback = (Error) -> Void

View File

@@ -0,0 +1,59 @@
//
// File.swift
//
//
// Created by Michael Ong on 2/3/23.
//
import Foundation
import UIKit
public extension UIControl {
private var touchUpInsideIdentifier: UIAction.Identifier {
.init(rawValue: "guni_action_pressed")
}
@discardableResult
func pressed(
_ action: @escaping VoidCallbackAsync,
showFailureOn failure: FailureCallable? = nil,
showLoading: Bool = false,
resetLoading: Bool = true
) -> Self {
removeAction(identifiedBy: touchUpInsideIdentifier, for: .touchUpInside)
addAction(UIAction(identifier: touchUpInsideIdentifier) { [unowned self, weak failure] _ in
if showLoading {
if let loadable = self as? ButtonLoadable {
loadable.isLoading = true
} else {
isEnabled = false
}
}
Task { @MainActor in
do {
try await action()
} catch {
failure?.showFailure(error)
if let loadable = self as? ButtonLoadable {
loadable.isLoading = false
} else {
isEnabled = true
}
}
if resetLoading {
if let loadable = self as? ButtonLoadable {
loadable.isLoading = false
} else {
isEnabled = true
}
}
}
}, for: .touchUpInside)
return self
}
}

View File

@@ -0,0 +1,55 @@
//
// File.swift
//
//
// Created by Michael Ong on 2/3/23.
//
import Foundation
import UIKit
import makataForm
import makataInteraction
public extension UITextField {
func textChanges(_ action: Binding<some AnyObject, String>) -> Lifetimeable {
text = action.initialValue
return TextChangesProxy(source: self, action: action.action)
}
@discardableResult
func textChanges(_ action: Binding<some AnyObject, String>, lifetime: inout Lifetimeable?) -> Self {
lifetime = textChanges(action)
return self
}
}
extension UITextField {
private class TextChangesProxy: Lifetimeable {
let identifier = UIAction.Identifier("textfield.changes")
weak var source: UITextField!
init(source: UITextField, action: @escaping (String) throws -> String) {
self.source = source
source.addAction(UIAction(identifier: identifier) { [unowned source] _ in
do {
source.text = try action(source.text ?? "")
} catch {
print("TextChangesProxy: Unhandled failure in setting value to text field.")
print("If you intend to catch this error, change your target type to be boxed in a `PartialValue`.")
}
}, for: .allEditingEvents)
}
deinit {
cancel()
}
func cancel() {
source?.removeAction(identifiedBy: identifier, for: .allEditingEvents)
}
}
}

View File

@@ -0,0 +1,42 @@
// view-builder.swift
//
// Code Copyright Buslo Collective
// Created 2/3/23
//
// File.swift
//
//
// Created by Michael Ong on 2/3/23.
//
import Foundation
import UIKit
@resultBuilder
public enum ComponentBuilder {
public typealias Component = [UIView]
public static func buildExpression(_ expression: some UIView) -> Component {
[expression]
}
public static func buildEither(first component: ComponentBuilder.Component) -> ComponentBuilder.Component {
component
}
public static func buildEither(second component: ComponentBuilder.Component) -> ComponentBuilder.Component {
component
}
public static func buildOptional(_ component: ComponentBuilder.Component?) -> ComponentBuilder.Component {
component ?? []
}
public static func buildArray(_ components: [ComponentBuilder.Component]) -> ComponentBuilder.Component {
components.flatMap { $0 }
}
public static func buildBlock(_ components: Component...) -> Component {
components.flatMap { $0 }
}
}

View File

@@ -0,0 +1,163 @@
// view.swift
//
// Code Copyright Buslo Collective
// Created 2/3/23
import Foundation
import makataInteraction
import SnapKit
import UIKit
extension UIView: Assignable, Attributable, Withable {}
public extension UIView {
static func stackFiller() -> UIView {
let view = UIView()
view.setContentHuggingPriority(.init(rawValue: 0), for: .horizontal)
view.setContentHuggingPriority(.init(rawValue: 0), for: .vertical)
return view
}
static func stackBlock(height: CGFloat = 8) -> UIView {
let view = UIView()
view.heightAnchor.constraint(equalToConstant: height).isActive = true
return view
}
convenience init(containing: some UIView, constraints: (ConstraintMaker) -> Void) {
self.init(frame: .zero)
addSubview(view: containing, constraints: constraints)
}
@discardableResult
func autoFocus() -> Self {
becomeFirstResponder()
return self
}
@discardableResult
func sized(_ size: CGSize, rounded: Bool = false) -> Self {
translatesAutoresizingMaskIntoConstraints = false
layer.cornerRadius = rounded ? size.height / 2 : 0
layer.masksToBounds = true
snp.makeConstraints { make in
if !size.width.isNaN {
make.width
.equalTo(size.width)
.priority(.required)
}
if !size.height.isNaN {
make.height
.equalTo(size.height)
.priority(.required)
}
}
return self
}
@discardableResult
func backgroundColor(_ color: UIColor) -> Self {
backgroundColor = color
return self
}
@discardableResult
func hidden() -> Self {
isHidden = true
return self
}
@discardableResult
func visible() -> Self {
isHidden = false
return self
}
@discardableResult
func addSubview(view: some UIView, constraints: (ConstraintMaker) -> Void) -> Self {
view.translatesAutoresizingMaskIntoConstraints = false
addSubview(view)
view.snp.makeConstraints(constraints)
return self
}
}
public extension UIStackView {
static func horizontal(
spacing: CGFloat = 8,
alignment: UIStackView.Alignment = .leading,
distribution: UIStackView.Distribution = .fill,
@ComponentBuilder components: () -> ComponentBuilder.Component
) -> Self {
let stack = Self(frame: .zero)
stack.axis = .horizontal
stack.spacing = spacing
stack.alignment = alignment
stack.distribution = distribution
stack.insetsLayoutMarginsFromSafeArea = false
stack.setContentHuggingPriority(.required, for: .vertical)
components().forEach(stack.addArrangedSubview)
return stack
}
static func vertical(
spacing: CGFloat = 8,
alignment: UIStackView.Alignment = .leading,
distribution: UIStackView.Distribution = .fill,
@ComponentBuilder components: () -> ComponentBuilder.Component
) -> Self {
let stack = Self(frame: .zero)
stack.axis = .vertical
stack.spacing = spacing
stack.alignment = alignment
stack.distribution = distribution
stack.insetsLayoutMarginsFromSafeArea = false
stack.setContentHuggingPriority(.required, for: .vertical)
components().forEach(stack.addArrangedSubview)
return stack
}
convenience init(
axis: NSLayoutConstraint.Axis,
spacing: CGFloat = 8,
alignment: UIStackView.Alignment = .leading,
distribution: UIStackView.Distribution = .fill,
@ComponentBuilder components: () -> ComponentBuilder.Component
) {
self.init(frame: .zero)
self.axis = axis
self.spacing = spacing
self.alignment = alignment
self.distribution = distribution
insetsLayoutMarginsFromSafeArea = false
components().forEach(addArrangedSubview)
}
@discardableResult
func margins(_ insets: UIEdgeInsets, relativeToSafeArea: Bool = false) -> Self {
isLayoutMarginsRelativeArrangement = true
insetsLayoutMarginsFromSafeArea = relativeToSafeArea
layoutMargins = insets
return self
}
}