d
This commit is contained in:
130
src/App.swift
130
src/App.swift
@@ -11,6 +11,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate
|
|||||||
) -> Bool {
|
) -> Bool {
|
||||||
window = UIWindow(frame: UIScreen.main.bounds)
|
window = UIWindow(frame: UIScreen.main.bounds)
|
||||||
let vc = UIViewController()
|
let vc = UIViewController()
|
||||||
|
addSwiftUIViewAsChild(swiftUIView: V(), parent: vc.view)
|
||||||
vc.view.backgroundColor = .gray
|
vc.view.backgroundColor = .gray
|
||||||
window?.rootViewController = vc
|
window?.rootViewController = vc
|
||||||
window?.backgroundColor = UIColor.white
|
window?.backgroundColor = UIColor.white
|
||||||
@@ -19,3 +20,132 @@ class AppDelegate: UIResponder, UIApplicationDelegate
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Bus.Pipe.swift
|
||||||
|
public extension Bus {
|
||||||
|
final class BindingPipe<T: Equatable>: ObservableObject {
|
||||||
|
public let input = PassthroughSubject<T, Never>()
|
||||||
|
public let output = PassthroughSubject<T, Never>()
|
||||||
|
@Published var value: T
|
||||||
|
var isInput = false
|
||||||
|
var subscriptions = [AnyCancellable]()
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
print("ИГР BusBP.DEinit(\(Unmanaged.passUnretained(self).toOpaque()))")
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
_ defaultValue: T,
|
||||||
|
_ keyApp: String,
|
||||||
|
_ keyUI: String
|
||||||
|
) {
|
||||||
|
value = defaultValue
|
||||||
|
/**/dbg("ИГР BusBP.init(\(Unmanaged.passUnretained(self).toOpaque())) keyA: '\(keyApp)'")
|
||||||
|
|
||||||
|
// TextField -> output.
|
||||||
|
$value
|
||||||
|
.sink { [weak self] v in
|
||||||
|
assert(Thread.isMainThread)
|
||||||
|
// Ignore values received from input.
|
||||||
|
// Only accept UI provided values.
|
||||||
|
guard
|
||||||
|
let self,
|
||||||
|
!self.isInput
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.output.send(v)
|
||||||
|
}
|
||||||
|
.store(in: &subscriptions)
|
||||||
|
|
||||||
|
// input -> TextField.
|
||||||
|
input
|
||||||
|
.sink { [weak self] v in
|
||||||
|
assert(Thread.isMainThread)
|
||||||
|
guard let self else { return }
|
||||||
|
/**/dbg("ИГР BusBP.init input: '\(v)'")
|
||||||
|
self.isInput = true
|
||||||
|
self.value = v
|
||||||
|
self.isInput = false
|
||||||
|
}
|
||||||
|
.store(in: &subscriptions)
|
||||||
|
|
||||||
|
// Оповещаем в мир об изменениях от UI.
|
||||||
|
Bus.sendAsync(
|
||||||
|
&subscriptions,
|
||||||
|
keyUI,
|
||||||
|
output.eraseToAnyPublisher()
|
||||||
|
)
|
||||||
|
|
||||||
|
Bus.receive(
|
||||||
|
&subscriptions,
|
||||||
|
[keyApp],
|
||||||
|
{ [weak self] _, v in
|
||||||
|
self?.input.send(v)
|
||||||
|
/**/print("ИГР BusBP.init receive: '\(v)'")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BusSlot.swift
|
||||||
|
var busHolder = Bus.Service()
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// MARK: - MeetupX module
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
enum MeetupId {
|
||||||
|
enum Keys: String, RawRepresentable {
|
||||||
|
case meetupIdTextApp
|
||||||
|
case meetupIdTextUI
|
||||||
|
}
|
||||||
|
|
||||||
|
static func shouldFormat(_ s: String) -> String? {
|
||||||
|
return s.components(separatedBy: NSCharacterSet.decimalDigits.inverted).reduce("") { $0 + $1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MeetupIdFormatter {
|
||||||
|
var subscriptions = [AnyCancellable]()
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
/**/dbg("ИГР MeetupIF.DEinit")
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
Bus.receive(
|
||||||
|
&subscriptions,
|
||||||
|
[Keys.meetupIdTextUI.rawValue],
|
||||||
|
{ [weak self] k, v in self?.handleFormatting(k, v) }
|
||||||
|
)
|
||||||
|
/**/dbg("ИГР MeetupIF.init")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleFormatting(_: String, _ value: String) {
|
||||||
|
let out = MeetupId.shouldFormat(value)
|
||||||
|
/**/dbg("ИГР MeetupIF.handleF out/dt: '\(out)'/'\(Date())'")
|
||||||
|
Bus.Service.singleton?.send(Keys.meetupIdTextApp.rawValue, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct V: View {
|
||||||
|
/*@StateObject*/ var fmt = MeetupIdFormatter()
|
||||||
|
@StateObject var txt = Bus.BindingPipe("", Keys.meetupIdTextApp.rawValue, Keys.meetupIdTextUI.rawValue)
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
Text("Hi, the text is: '\(txt.value)'")
|
||||||
|
.fontWeight(.bold)
|
||||||
|
TextField("Placeholder", text: $txt.value)
|
||||||
|
.padding(8)
|
||||||
|
.border(Color.blue, width: 2)
|
||||||
|
}
|
||||||
|
.frame(width: 320)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|||||||
55
src/Bus.swift
Normal file
55
src/Bus.swift
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import Combine
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public enum Bus { }
|
||||||
|
|
||||||
|
extension Bus {
|
||||||
|
public final class Service {
|
||||||
|
static private(set) weak var singleton: Service?
|
||||||
|
let broadcaster = PassthroughSubject<(key: String, value: Any), Never>()
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
Self.singleton = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
Self.singleton = self
|
||||||
|
}
|
||||||
|
|
||||||
|
func send(_ key: String, _ value: Any) {
|
||||||
|
broadcaster.send((key, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Bus {
|
||||||
|
static func receive<T>(
|
||||||
|
_ subscriptions: inout Set<AnyCancellable>,
|
||||||
|
_ keys: Set<String>,
|
||||||
|
_ handler: @escaping ((String, T) -> Void)
|
||||||
|
) {
|
||||||
|
Service.singleton?.broadcaster
|
||||||
|
.compactMap { v -> (String, T)? in
|
||||||
|
guard
|
||||||
|
keys.contains(v.key),
|
||||||
|
let value = v.value as? T
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return (v.key, value)
|
||||||
|
}
|
||||||
|
.sink { v in handler(v.0, v.1) }
|
||||||
|
.store(in: &subscriptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func sendAsync<T: Equatable>(
|
||||||
|
_ subscriptions: inout Set<AnyCancellable>,
|
||||||
|
_ key: String,
|
||||||
|
_ node: AnyPublisher<T, Never>
|
||||||
|
) {
|
||||||
|
node
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { v in Service.singleton?.send(key, v) }
|
||||||
|
.store(in: &subscriptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/V.swift
Normal file
20
src/V.swift
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct V: View {
|
||||||
|
@StateObject var vm = VM()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Text("Check text field:")
|
||||||
|
Text("'\(vm.text)'")
|
||||||
|
.fontWeight(.bold)
|
||||||
|
}
|
||||||
|
TextField("Placeholder", text: $vm.text)
|
||||||
|
.padding(8)
|
||||||
|
.border(Color.red, width: 2)
|
||||||
|
}
|
||||||
|
.frame(width: 320)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/VM.swift
Normal file
38
src/VM.swift
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import Combine
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
final class VM: ObservableObject {
|
||||||
|
@Published var text = ""
|
||||||
|
let changeText = PassthroughSubject<String, Never>()
|
||||||
|
var subscriptions = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Уведомляем об изменении текста.
|
||||||
|
$text
|
||||||
|
.sink { [weak self] v in self?.changeText.send(v) }
|
||||||
|
.store(in: &subscriptions)
|
||||||
|
|
||||||
|
// Интерпретируем текст с задержкой, потому что:
|
||||||
|
// 1. смена @Published в ту же секунду игнорируется
|
||||||
|
// полем ввода, т.е. нужна задержка
|
||||||
|
// 2. ожидаем окончания ввода (спама), чтобы
|
||||||
|
// обработать введённое без потерь из-за конфликта
|
||||||
|
// старых данных и новых.
|
||||||
|
changeText
|
||||||
|
.debounce(for: .seconds(0.3), scheduler: DispatchQueue.main)
|
||||||
|
.sink { [weak self] v in
|
||||||
|
/*
|
||||||
|
guard
|
||||||
|
let out = MeetupId.shouldFormat(v),
|
||||||
|
v != out
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let out = v
|
||||||
|
/**/print("ИГР TFCVM.init changeT in/out: '\(v)'/'\(out)'")
|
||||||
|
self?.text = out
|
||||||
|
}
|
||||||
|
.store(in: &subscriptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/addSwiftUIAsChild.swift
Normal file
19
src/addSwiftUIAsChild.swift
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
public func addSwiftUIViewAsChild<Type>(
|
||||||
|
swiftUIView: Type,
|
||||||
|
parent: UIView
|
||||||
|
) -> UIHostingController<Type> {
|
||||||
|
let childVC = UIHostingController(rootView: swiftUIView)
|
||||||
|
childVC.view.backgroundColor = .clear
|
||||||
|
parent.addSubview(childVC.view)
|
||||||
|
childVC.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
childVC.view.topAnchor.constraint(equalTo: parent.topAnchor),
|
||||||
|
childVC.view.bottomAnchor.constraint(equalTo: parent.bottomAnchor),
|
||||||
|
childVC.view.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
|
||||||
|
childVC.view.trailingAnchor.constraint(equalTo: parent.trailingAnchor),
|
||||||
|
])
|
||||||
|
return childVC
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user