Михаил Капелько 11 months ago
parent
commit
30efa129a8
5 changed files with 262 additions and 0 deletions
  1. +130
    -0
      src/App.swift
  2. +55
    -0
      src/Bus.swift
  3. +20
    -0
      src/V.swift
  4. +38
    -0
      src/VM.swift
  5. +19
    -0
      src/addSwiftUIAsChild.swift

+ 130
- 0
src/App.swift View File

@@ -11,6 +11,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate
) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
let vc = UIViewController()
addSwiftUIViewAsChild(swiftUIView: V(), parent: vc.view)
vc.view.backgroundColor = .gray
window?.rootViewController = vc
window?.backgroundColor = UIColor.white
@@ -19,3 +20,132 @@ class AppDelegate: UIResponder, UIApplicationDelegate
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
- 0
src/Bus.swift View 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
- 0
src/V.swift View 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
- 0
src/VM.swift View 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
- 0
src/addSwiftUIAsChild.swift View 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
}

Loading…
Cancel
Save