This commit is contained in:
Михаил Капелько
2023-12-28 13:33:20 +03:00
parent a1ec08f280
commit 16026a7b47
21 changed files with 26 additions and 16 deletions

14
Modules/BusX/BusX.podspec Normal file
View File

@@ -0,0 +1,14 @@
Pod::Spec.new do |s|
s.name = 'BusX'
s.version = '2023.12.28'
s.license = 'IVCS'
s.summary = 'Шина общения элементов приложения'
s.homepage = 'IVCS'
s.author = 'IVCS'
s.source = { :git => 'https://fake.com/FAKE.git', :tag => s.version }
s.source_files = 'src/**/*.swift'
s.swift_version = '5.2'
s.ios.deployment_target = '14.0'
end

View File

@@ -0,0 +1,30 @@
extension Bus {
/// Пропускаем далее предоставленные ключи.
static func convertKeyValue<T>(
_ keys: Set<String>,
_ v: (key: String, value: Any)
) -> (String, T)? {
guard
keys.contains(v.key),
let value = v.value as? T
else {
return nil
}
return (v.key, value)
}
/// Обрабатываем.
static func processKeysValue<Src, Dst>(
_ v: (key: String, value: Any),
_ keysIn: Set<String>,
_ handler: @escaping ((Src) -> Dst?)
) -> Dst? {
guard
keysIn.contains(v.key),
let vIn = v.value as? Src
else {
return nil
}
return handler(vIn)
}
}

View File

@@ -0,0 +1,25 @@
import Combine
public extension Bus {
final class Processor<Src, Dst> {
var subscriptions = [AnyCancellable]()
public init(
_ keyIn: String,
_ keyOut: String,
_ handler: @escaping ((Src) -> Dst?),
opt: [Option] = []
) {
Bus.process([keyIn], keyOut, handler, opt: opt, sub: &subscriptions)
}
public init(
_ keysIn: Set<String>,
_ keyOut: String,
_ handler: @escaping ((Src) -> Dst?),
opt: [Option] = []
) {
Bus.process(keysIn, keyOut, handler, opt: opt, sub: &subscriptions)
}
}
}

125
Modules/BusX/src/Bus.swift Normal file
View File

@@ -0,0 +1,125 @@
import Combine
import Foundation
public enum Bus { }
public extension Bus {
enum Option {
case async
}
}
extension Bus {
final class Service {
static let singleton = Service()
let events = PassthroughSubject<(key: String, value: Any), Never>()
var subscriptions = [AnyCancellable]()
func send(_ key: String, _ value: Any) {
/**/print("ИГР BusS.send key/value: '\(key)'/'\(value)'")
events.send((key, value))
}
}
}
private extension Bus {
static func subscribe(
_ subscription: AnyCancellable?,
_ sub: UnsafeMutablePointer<[AnyCancellable]>?
) {
guard let subscription else { return }
if let sub = sub {
sub.pointee.append(subscription)
} else {
Service.singleton.subscriptions.append(subscription)
}
}
}
public extension Bus {
static func receive<T>(
_ keys: Set<String>,
_ handler: @escaping ((String, T) -> Void),
opt: [Option] = [],
sub: UnsafeMutablePointer<[AnyCancellable]>? = nil
) {
var subscription: AnyCancellable?
let isAsync = opt.contains(.async)
// Async.
if isAsync {
subscription = Service.singleton.events
.compactMap { convertKeyValue(keys, $0) }
.receive(on: DispatchQueue.main)
.sink { v in handler(v.0, v.1) }
}
// Async.
if !isAsync {
subscription = Service.singleton.events
.compactMap { convertKeyValue(keys, $0) }
.sink { v in handler(v.0, v.1) }
}
subscribe(subscription, sub)
}
static func send<T>(
_ key: String,
_ node: AnyPublisher<T, Never>,
opt: [Option] = [],
sub: UnsafeMutablePointer<[AnyCancellable]>? = nil
) {
var subscription: AnyCancellable?
let isAsync = opt.contains(.async)
// Async.
if isAsync {
subscription = node
.receive(on: DispatchQueue.main)
.sink { v in Service.singleton.send(key, v) }
}
// Sync.
if !isAsync {
subscription = node
.sink { v in Service.singleton.send(key, v) }
}
subscribe(subscription, sub)
}
static func send(_ key: String, _ value: Any) {
Service.singleton.send(key, value)
}
}
public extension Bus {
static func process<Src, Dst>(
_ keysIn: Set<String>,
_ keyOut: String,
_ handler: @escaping ((Src) -> Dst?),
opt: [Option] = [],
sub: UnsafeMutablePointer<[AnyCancellable]>? = nil
) {
var subscription: AnyCancellable?
let isAsync = opt.contains(.async)
// Async.
if isAsync {
subscription = Service.singleton.events
.compactMap { processKeysValue($0, keysIn, handler) }
.receive(on: DispatchQueue.main)
.sink { vOut in Service.singleton.send(keyOut, vOut) }
}
// Sync.
if !isAsync {
subscription = Service.singleton.events
.compactMap { processKeysValue($0, keysIn, handler) }
.sink { vOut in Service.singleton.send(keyOut, vOut) }
}
subscribe(subscription, sub)
}
}

30
Modules/BusX/src/unused Normal file
View File

@@ -0,0 +1,30 @@
import Combine
public extension Bus {
final class Receiver<T> {
var subscriptions = [AnyCancellable]()
public init(
_ keys: Set<String>,
_ handler: @escaping ((String, T) -> Void),
opt: [Option] = []
) {
Bus.receive(keys, handler, opt: opt, sub: &subscriptions)
}
}
}
import Combine
public extension Bus {
final class Sender<T> {
var subscriptions = [AnyCancellable]()
public init(
_ key: String,
_ node: AnyPublisher<T, Never>,
opt: [Option] = []
) {
Bus.send(key, node, opt: opt, sub: &subscriptions)
}
}
}

View File

@@ -0,0 +1,15 @@
Pod::Spec.new do |s|
s.name = 'CordX'
s.version = '2023.12.28'
s.license = 'IVCS'
s.summary = 'Упрощённое общение с шиной из SwiftUI'
s.homepage = 'IVCS'
s.author = 'IVCS'
s.source = { :git => 'https://fake.com/FAKE.git', :tag => s.version }
s.source_files = 'src/**/*.swift'
s.swift_version = '5.2'
s.ios.deployment_target = '14.0'
s.dependency 'BusX'
end

View File

@@ -0,0 +1,17 @@
import BusX
import Combine
extension Cord {
public final class Button: ObservableObject {
public let press = PassthroughSubject<Void, Never>()
var subscriptions = [AnyCancellable]()
public init(_ key: String) {
Bus.send(
key,
press.eraseToAnyPublisher(),
sub: &subscriptions
)
}
}
}

View File

@@ -0,0 +1,9 @@
extension Cord {
/// Пропускаем лишь значения от UI
///
/// - Returns: Значение без префиксов "a:"/"u:"
static func onlyUIText(_ s: String) -> String? {
guard s.hasPrefix("u:") else { return nil }
return String(s.dropFirst(2))
}
}

View File

@@ -0,0 +1,21 @@
import BusX
import Combine
extension Cord {
public final class Receive<T>: ObservableObject {
@Published public var value: T
var subscriptions = [AnyCancellable]()
public init(
_ key: String,
_ defaultValue: T
) {
value = defaultValue
Bus.receive(
[key],
{ [weak self] (_, v: T) in self?.value = v },
sub: &subscriptions
)
}
}
}

View File

@@ -0,0 +1,30 @@
import BusX
import Combine
import SwiftUI
extension Cord {
public final class TextField: ObservableObject {
@Published public var value = "a:"
var subscriptions = [AnyCancellable]()
public init(
_ textApp: String,
_ textUI: String
) {
Bus.send(
textUI,
$value
.removeDuplicates()
.compactMap(onlyUIText)
.eraseToAnyPublisher(),
sub: &subscriptions
)
Bus.receive(
[textApp],
{ [weak self] (_, v: String) in self?.value = "a:\(v)" },
sub: &subscriptions
)
}
}
}

View File

@@ -0,0 +1,19 @@
import Foundation
extension Cord {
public final class TextFieldValueOwner: Formatter {
public override func string(for obj: Any?) -> String? {
guard let str = obj as? String else { return nil }
return String(str.dropFirst(2))
}
public override func getObjectValue(
_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?,
for string: String,
errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?
) -> Bool {
obj?.pointee = "u:\(string)" as AnyObject
return true
}
}
}

View File

@@ -0,0 +1 @@
public enum Cord { }

View File

@@ -0,0 +1,16 @@
version: 2
model:
textApp: [String, ""]
textUI: [String, ""]
service:
actions:
modelBus
pipes:
textApp: [recent, K.meetupIdTextApp]
textUI: [recent, K.meetupIdTextUI]
world:
textApp: [ps]
textUI: [ps]

View File

@@ -0,0 +1,16 @@
Pod::Spec.new do |s|
s.name = 'MeetupIdX'
s.version = '2023.12.28'
s.license = 'IVCS'
s.summary = 'Окно ввода ID мероприятия'
s.homepage = 'IVCS'
s.author = 'IVCS'
s.source = { :git => 'https://fake.com/FAKE.git', :tag => s.version }
s.source_files = 'src/**/*.swift'
s.swift_version = '5.2'
s.ios.deployment_target = '14.0'
s.dependency 'BusX'
s.dependency 'CordX'
end

View File

@@ -0,0 +1,9 @@
public extension MeetupId {
enum K: String {
case meetupIdIsJoinAvailable
case meetupIdJoin
case meetupIdTextApp
case meetupIdTextUI
}
}

View File

@@ -0,0 +1,26 @@
import Foundation
public extension MeetupId {
static func onlyAllowJoin(_ s: String) -> Bool? {
s.hasPrefix("123")
}
static func onlyFormat(_ s: String) -> String? {
let digits = s.components(separatedBy: NSCharacterSet.decimalDigits.inverted).reduce("") { $0 + $1 }
var r = ""
var i = 0
// Делим каждые три цифры дефисом.
for v in digits {
r += String(v)
i = i + 1
if i % 3 == 0 {
r += "-"
}
}
// Исключаем дефис в конце.
if r.hasSuffix("-") {
r = String(r.dropLast(1))
}
return r
}
}

View File

@@ -0,0 +1 @@
public enum MeetupId { }

View File

@@ -0,0 +1,40 @@
import BusX
import CordX
import SwiftUI
extension MeetupId {
public struct V: View {
@StateObject var isJA = Cord.Receive(K.meetupIdIsJoinAvailable.rawValue, false)
@StateObject var join = Cord.Button(K.meetupIdJoin.rawValue)
@StateObject var txtF = Cord.TextField(K.meetupIdTextApp.rawValue, K.meetupIdTextUI.rawValue)
let test = Bus.Processor(K.meetupIdTextUI.rawValue, K.meetupIdIsJoinAvailable.rawValue, onlyAllowJoin)
public init() { }
public var body: some View {
VStack {
HStack {
Text("Check text field:")
Text("'\(txtF.value)'")
.fontWeight(.bold)
}
TextField("Binding-3", value: $txtF.value, formatter: Cord.TextFieldValueOwner())
.padding(8)
.border(Color.blue, width: 2)
Button(action: join.press.send) {
Text("Join")
.padding(8)
.border(
isJA.value ? Color.green : Color.gray,
width: isJA.value ? 4 : 1
)
}
.disabled(!isJA.value)
}
.frame(width: 320)
.padding()
}
}
}