d
This commit is contained in:
14
Modules/BusX/BusX.podspec
Normal file
14
Modules/BusX/BusX.podspec
Normal 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
|
||||
30
Modules/BusX/src/Bus.Aux.swift
Normal file
30
Modules/BusX/src/Bus.Aux.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
25
Modules/BusX/src/Bus.Processor.swift
Normal file
25
Modules/BusX/src/Bus.Processor.swift
Normal 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
125
Modules/BusX/src/Bus.swift
Normal 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
30
Modules/BusX/src/unused
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
15
Modules/CordX/CordX.podspec
Normal file
15
Modules/CordX/CordX.podspec
Normal 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
|
||||
17
Modules/CordX/src/Cord.Button.swift
Normal file
17
Modules/CordX/src/Cord.Button.swift
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Modules/CordX/src/Cord.Onlys.swift
Normal file
9
Modules/CordX/src/Cord.Onlys.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
21
Modules/CordX/src/Cord.Receive.swift
Normal file
21
Modules/CordX/src/Cord.Receive.swift
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
30
Modules/CordX/src/Cord.TextField.swift
Normal file
30
Modules/CordX/src/Cord.TextField.swift
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Modules/CordX/src/Cord.TextFieldValueOwner.swift
Normal file
19
Modules/CordX/src/Cord.TextFieldValueOwner.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
1
Modules/CordX/src/Cord.swift
Normal file
1
Modules/CordX/src/Cord.swift
Normal file
@@ -0,0 +1 @@
|
||||
public enum Cord { }
|
||||
16
Modules/MeetupIdX/MeetupId.yml
Normal file
16
Modules/MeetupIdX/MeetupId.yml
Normal 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]
|
||||
16
Modules/MeetupIdX/MeetupIdX.podspec
Normal file
16
Modules/MeetupIdX/MeetupIdX.podspec
Normal 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
|
||||
9
Modules/MeetupIdX/src/MeetupId.K.swift
Normal file
9
Modules/MeetupIdX/src/MeetupId.K.swift
Normal file
@@ -0,0 +1,9 @@
|
||||
public extension MeetupId {
|
||||
enum K: String {
|
||||
case meetupIdIsJoinAvailable
|
||||
case meetupIdJoin
|
||||
case meetupIdTextApp
|
||||
case meetupIdTextUI
|
||||
}
|
||||
}
|
||||
|
||||
26
Modules/MeetupIdX/src/MeetupId.Onlys.swift
Normal file
26
Modules/MeetupIdX/src/MeetupId.Onlys.swift
Normal 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
|
||||
}
|
||||
}
|
||||
1
Modules/MeetupIdX/src/MeetupId.swift
Normal file
1
Modules/MeetupIdX/src/MeetupId.swift
Normal file
@@ -0,0 +1 @@
|
||||
public enum MeetupId { }
|
||||
40
Modules/MeetupIdX/src/V.swift
Normal file
40
Modules/MeetupIdX/src/V.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user