34 Commits
single ... many

Author SHA1 Message Date
Михаил Капелько
6be169e676 d 2024-01-19 18:33:16 +03:00
Михаил Капелько
33d81438f5 d 2024-01-19 18:27:55 +03:00
Михаил Капелько
21fcb9af4a d 2024-01-19 18:21:02 +03:00
Михаил Капелько
f5fd715e3e d 2024-01-19 18:20:43 +03:00
Михаил Капелько
1f17414db4 d 2024-01-19 18:17:27 +03:00
Михаил Капелько
806c04bcc4 d 2024-01-19 18:14:19 +03:00
Михаил Капелько
18e098141d d 2024-01-19 18:09:28 +03:00
Михаил Капелько
16c798c31c d 2024-01-19 18:08:50 +03:00
Михаил Капелько
713f206714 d 2024-01-19 18:06:36 +03:00
Михаил Капелько
59f1da6b87 d 2024-01-18 18:52:28 +03:00
Михаил Капелько
496c34a89f d 2024-01-18 18:46:09 +03:00
Михаил Капелько
a7f5161e5e d 2024-01-18 18:27:39 +03:00
Михаил Капелько
30eb15950a d 2024-01-17 18:53:09 +03:00
Михаил Капелько
be8c1bf6bd d 2024-01-17 18:50:32 +03:00
Михаил Капелько
b3a386cde7 d 2024-01-17 18:47:21 +03:00
Михаил Капелько
9e941f01a5 d 2024-01-17 18:46:57 +03:00
Михаил Капелько
625e2244df d 2024-01-17 18:44:04 +03:00
Михаил Капелько
e0f9938367 d 2024-01-17 18:42:21 +03:00
Михаил Капелько
5cd4ad6d74 d 2024-01-17 18:41:10 +03:00
Михаил Капелько
cca1543856 d 2024-01-17 18:34:18 +03:00
Михаил Капелько
a5bff85223 d 2024-01-17 18:12:27 +03:00
Михаил Капелько
1a731e2fad d 2024-01-17 18:12:00 +03:00
Михаил Капелько
d3a78ee599 d 2024-01-17 18:05:34 +03:00
Михаил Капелько
1d3419d5df d 2024-01-17 18:04:35 +03:00
Михаил Капелько
ee57d253bc d 2024-01-17 18:01:41 +03:00
Михаил Капелько
a76df252db d 2024-01-16 19:19:33 +03:00
Михаил Капелько
4033fba38c d 2024-01-16 19:15:35 +03:00
Михаил Капелько
76cef40423 d 2024-01-16 19:08:46 +03:00
Михаил Капелько
061548b8b4 d 2024-01-16 19:02:32 +03:00
Михаил Капелько
deb238bb21 d 2024-01-16 18:55:00 +03:00
Михаил Капелько
273298e4c9 d 2024-01-16 18:51:18 +03:00
Михаил Капелько
a0ad8da8df d 2024-01-15 19:02:23 +03:00
Михаил Капелько
1779eb3efe d 2024-01-15 18:41:58 +03:00
Михаил Капелько
153e5919ce empty 2024-01-15 18:39:41 +03:00
25 changed files with 228 additions and 342 deletions

1
Modules/BusX Symbolic link
View File

@@ -0,0 +1 @@
../../../../u/Modules/BusX

View File

@@ -1,31 +0,0 @@
import Combine
public extension Bus {
final class Async<Src, Dst> {
let v = PassthroughSubject<Src, Never>()
var subscriptions = [AnyCancellable]()
public init(
_ handler: @escaping ((Src) -> Dst?),
_ src: String,
_ dst: String
) {
// Вход.
Bus.receiveSync(
[src],
{ [weak self] _, v in self?.v.send(v) },
&subscriptions
)
// Выход.
Bus.sendSync(
dst,
v
.compactMap { (v: Src) in handler(v) }
// Асинхронно.
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher(),
&subscriptions
)
}
}
}

View File

@@ -1,29 +0,0 @@
public extension Bus {
/// Пропускаем далее единственный ключ.
static func convertKeyValue<T>(
_ key: String,
_ v: (key: String, value: Any)
) -> (String, T)? {
guard
key == v.key,
let value = v.value as? T
else {
return nil
}
return (key, value)
}
/// Пропускаем далее множество ключей.
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)
}
}

View File

@@ -1,31 +0,0 @@
import Combine
public extension Bus {
final class Debounce<Src, Dst> {
let v = PassthroughSubject<Src, Never>()
var subscriptions = [AnyCancellable]()
public init(
_ handler: @escaping ((Src) -> Dst?),
_ sec: Double,
_ src: String,
_ dst: String
) {
// Вход.
Bus.receiveSync(
[src],
{ [weak self] _, v in self?.v.send(v) },
&subscriptions
)
// Выход.
Bus.sendSync(
dst,
v
.debounce(for: .seconds(sec), scheduler: DispatchQueue.main)
.compactMap { (v: Src) in handler(v) }
.eraseToAnyPublisher(),
&subscriptions
)
}
}
}

View File

@@ -1,31 +0,0 @@
import Combine
public extension Bus {
final class Delay<Src, Dst> {
let v = PassthroughSubject<Src, Never>()
var subscriptions = [AnyCancellable]()
public init(
_ handler: @escaping ((Src) -> Dst?),
_ sec: Double,
_ src: String,
_ dst: String
) {
// Вход.
Bus.receiveSync(
[src],
{ [weak self] _, v in self?.v.send(v) },
&subscriptions
)
// Выход.
Bus.sendSync(
dst,
v
.delay(for: .seconds(sec), scheduler: DispatchQueue.main)
.compactMap { (v: Src) in handler(v) }
.eraseToAnyPublisher(),
&subscriptions
)
}
}
}

View File

@@ -1,29 +0,0 @@
import Combine
public extension Bus {
final class Sync<Src, Dst> {
let v = PassthroughSubject<Src, Never>()
var subscriptions = [AnyCancellable]()
public init(
_ handler: @escaping ((Src) -> Dst?),
_ src: String,
_ dst: String
) {
// Вход.
Bus.receiveSync(
[src],
{ [weak self] _, v in self?.v.send(v) },
&subscriptions
)
// Выход.
Bus.sendSync(
dst,
v
.compactMap { (v: Src) in handler(v) }
.eraseToAnyPublisher(),
&subscriptions
)
}
}
}

View File

@@ -1,56 +0,0 @@
import Combine
import Foundation
public enum Bus {
private static let e = PassthroughSubject<(key: String, value: Any), Never>()
public static var events: AnyPublisher<(key: String, value: Any), Never> {
e.eraseToAnyPublisher()
}
}
public extension Bus {
/// Асинхронно обрабатываем входящие события из шины.
static func receiveAsync<T>(
_ keys: Set<String>,
_ handler: @escaping ((String, T) -> Void),
_ subscriptions: inout [AnyCancellable]
) {
e
.compactMap { convertKeyValue(keys, $0) }
.receive(on: DispatchQueue.main)
.sink { v in handler(v.0, v.1) }
.store(in: &subscriptions)
}
/// Синхронно обрабатываем входящие события из шины.
static func receiveSync<T>(
_ keys: Set<String>,
_ handler: @escaping ((String, T) -> Void),
_ subscriptions: inout [AnyCancellable]
) {
e
.compactMap { convertKeyValue(keys, $0) }
.sink { v in handler(v.0, v.1) }
.store(in: &subscriptions)
}
/// Синхронно отправляем события из узла в шину.
///
/// Для асинхронной отправки достаточно добавить оператор `receive(on:)`
/// в цепочке параметра `node`
static func sendSync<T>(
_ key: String,
_ node: AnyPublisher<T, Never>,
_ subscriptions: inout [AnyCancellable]
) {
node
.sink { v in e.send((key, v)) }
.store(in: &subscriptions)
}
/// Единоразово синхронно отправляем событие в шину.
static func send(_ key: String, _ value: Any) {
e.send((key, value))
}
}

View File

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

View File

@@ -1,16 +0,0 @@
import Combine
extension BusUI {
public final class Button: ObservableObject {
public let v = PassthroughSubject<Void, Never>()
var subscriptions = [AnyCancellable]()
public init(_ key: String) {
Bus.sendSync(
key,
v.map { true }.eraseToAnyPublisher(),
&subscriptions
)
}
}
}

View File

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

View File

@@ -1,22 +0,0 @@
import Foundation
extension BusUI {
public final class TextFieldSource: Formatter {
/// Выдаём для отображения очищенную от источника строку.
public override func string(for obj: Any?) -> String? {
guard let str = obj as? String else { return nil }
return String(str.dropFirst(2))
}
/// Выдаём для использования кодом строку с указанием источника
/// в виде пользователя `u:`
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

@@ -1,20 +0,0 @@
import Combine
extension BusUI {
public final class Value<T>: ObservableObject {
@Published public var v: T
var subscriptions = [AnyCancellable]()
public init(
_ key: String,
_ defaultValue: T
) {
v = defaultValue
Bus.receiveSync(
[key],
{ [weak self] (_, v: T) in self?.v = v },
&subscriptions
)
}
}
}

View File

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

View File

@@ -1,14 +0,0 @@
Pod::Spec.new do |s|
s.name = 'BusX'
s.version = '2023.12.30'
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 = '**/**/*.swift'
s.swift_version = '5.2'
s.ios.deployment_target = '14.0'
end

View File

@@ -11,8 +11,8 @@ extension MeetupId {
public init() {
processors = [
Bus.Debounce(shouldResetText, 0.2, K.M, K.textApp),
Bus.Delay(shouldFinishLoading, 5, K.M, K.finishLoading),
Bus.ConstDebounce(shouldResetText, 0.2, K.M, K.textApp),
Bus.ConstDelay(shouldFinishLoading, 5, K.M, K.finishLoading),
Bus.Sync(shouldEnableJoin, K.M, K.isJoinAvailable),
Bus.Sync(shouldResetLoading, K.M, K.isLoading)
]

View File

@@ -0,0 +1,8 @@
public extension Mic {
enum K {
public static let activityDate = "Mic.activityDate"
public static let isActive = "Mic.isActive"
public static let MI = "Mic.Model.Item"
public static let timeout = "Mic.timeout"
}
}

View File

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

View File

@@ -0,0 +1,29 @@
public extension MicItem {
static func shouldResetActivity(_ c: MicItemContext) -> Bool? {
if
c.activityDate.isRecent,
let end = c.activityDate.value
{
let now = Date()
return now < end
}
if c.timeout {
return false
}
return nil
}
static func shouldResetTimeout(_ c: MicItemContext) -> TimeInterval? {
if
c.activityDate.isRecent,
let end = c.activityDate.value
{
let timeout = end.timeIntervalSinceNow
return timeout > 0 ? timeout : nil
}
return nil
}
}

View File

@@ -0,0 +1,35 @@
import BusX
import SwiftUI
extension MicItem {
public struct V: View {
var id: String?
@StateObject var isActive = BusUI.Value(K.isActive, false)
let proc: [Any]
public init(_ id: String? = nil) {
self.id = id
self.proc = [
Bus.Delay(shouldResetTimeout, K.MI, K.timeout, id),
Bus.Sync(shouldResetActivity, K.MI, K.isActive, id),
MicItem.Controller(id)
]
}
public var body: some View {
Text("Mic activity")
.padding(8)
.border(
isActive.v ? Color.black : Color.gray,
width: isActive.v ? 3 : 1
)
.animation(.easeInOut(duration: 0.3))
.onAppear {
isActive.id = id
}
.onChange(of: id) { newValue in
isActive.id = newValue
}
}
}
}

View File

@@ -0,0 +1,57 @@
import BusX
import MPAKX
public protocol MicItemContext {
var activityDate: MPAK.Recent<Date?> { get }
var timeout: Bool { get }
}
extension MicItem {
public struct Model: MicItemContext {
public var activityDate: MPAK.Recent<Date?> = .init(nil)
public var timeout: Bool = false
}
}
extension MicItem {
final class Controller: MPAK.Controller<MicItem.Model> {
init(_ id: String? = nil) {
var sid = ""
if let id {
sid = " : \(id)"
}
/**/print("MicIC.init\(sid)")
super.init(
MicItem.Model(),
debugClassName: "MicICtrl",
debugLog: { print("\($0)\(sid)") }
)
// Нижеследующее предстоит сгенерить.
m
.sink { v in Bus.send(Bus.keyId(K.MI, id), v) }
.store(in: &subscriptions)
pipeValue(
dbg: "activityD",
sub: nil,
Bus.events.compactMap { Bus.convertKeyValue(Bus.keyId(K.activityDate, id), $0) }.map { (k: String, v: Date?) in v }.eraseToAnyPublisher(),
{
$0.activityDate.value = $1
$0.activityDate.isRecent = true
},
{ m, _ in m.activityDate.isRecent = false }
)
pipe(
dbg: "timeout",
sub: nil,
Bus.events.compactMap { Bus.convertKeyValue(Bus.keyId(K.timeout, id), $0) }.map { (k: String, v: Bool) in v }.eraseToAnyPublisher(),
{ $0.timeout = true },
{ $0.timeout = false }
)
}
}
}

View File

@@ -0,0 +1,3 @@
public enum MicItem { }
typealias K = Mic.K

17
Modules/MicX/MicX.podspec Normal file
View File

@@ -0,0 +1,17 @@
Pod::Spec.new do |s|
s.name = 'MicX'
s.version = '2024.01.15'
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 = '**/**/*.{swift,yml}'
s.swift_version = '5.2'
s.ios.deployment_target = '14.0'
s.dependency 'AELog'
s.dependency 'BusX'
s.dependency 'MPAKX'
end

View File

@@ -6,6 +6,7 @@ pod 'AELog'
pod 'BusX', :path => '../Modules/BusX'
pod 'MeetupIdX', :path => '../Modules/MeetupIdX'
pod 'MicX', :path => '../Modules/MicX'
pod 'MPAKX', :path => '../Modules/MPAKX'
target 'pesochnicza' do

View File

@@ -1,16 +1,21 @@
PODS:
- AELog (0.6.3)
- BusX (2023.12.30)
- BusX (2024.01.16)
- MeetupIdX (2023.12.31):
- AELog
- BusX
- MPAKX
- MicX (2024.01.15):
- AELog
- BusX
- MPAKX
- MPAKX (2023.12.15)
DEPENDENCIES:
- AELog
- BusX (from `../Modules/BusX`)
- MeetupIdX (from `../Modules/MeetupIdX`)
- MicX (from `../Modules/MicX`)
- MPAKX (from `../Modules/MPAKX`)
SPEC REPOS:
@@ -22,15 +27,18 @@ EXTERNAL SOURCES:
:path: "../Modules/BusX"
MeetupIdX:
:path: "../Modules/MeetupIdX"
MicX:
:path: "../Modules/MicX"
MPAKX:
:path: "../Modules/MPAKX"
SPEC CHECKSUMS:
AELog: f732b70f7a9d1b4c6a3676304192b3908f362133
BusX: d11e857e9cb762f649ee9f88fd5a4f8fbf5bf96b
BusX: 7c7675152d44023781c7d46cda3617ceee41312f
MeetupIdX: 2fa9fb27717aa8878ff495c1abe960c96e524308
MicX: 6ffa81f29a477a66cde277e3ade3de8ce2d47b91
MPAKX: dc592434f55edf34709f6e4f37c9ec90dcd95185
PODFILE CHECKSUM: ff31073a9b868750f1cfabf6c1c740dbf32d4cb1
PODFILE CHECKSUM: 392946a4062b4b9d773ce17e4479d0040d242551
COCOAPODS: 1.13.0

View File

@@ -1,35 +1,79 @@
import BusX
import Combine
import MeetupIdX
import MicX
import SwiftUI
import UIKit
struct Content: View {
var body: some View {
MeetupId.V()
Divider()
MeetupId.V()
MicItem.V()
MicItem.V("1")
MicItem.V("2")
}
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate
{
let meetupIS = MeetupId.Service(.init())
var window: UIWindow?
let meetupIS = MeetupId.Service(.init())
var window: UIWindow?
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
let vc = UIViewController()
addSwiftUIViewAsChild(swiftUIView: Content(), parent: vc.view)
vc.view.backgroundColor = .white
window?.rootViewController = vc
window?.backgroundColor = UIColor.white
window?.makeKeyAndVisible()
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
let vc = UIViewController()
addSwiftUIViewAsChild(swiftUIView: Content(), parent: vc.view)
vc.view.backgroundColor = .white
window?.rootViewController = vc
window?.backgroundColor = UIColor.white
window?.makeKeyAndVisible()
/**/print("ИГР UUID:", UUID().uuidString)
return true
/**/print("ИГР App.didFLWO")
testMic1()
//testMic2()
testMic2_id("1")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.testMic2_id("2")
}
return true
}
func testMic1() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
Bus.send(Mic.K.isActive, true)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
Bus.send(Mic.K.isActive, false)
}
}
func testMic2() {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
Bus.send(Mic.K.activityDate, Date() + 2)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
Bus.send(Mic.K.activityDate, Date() + 2)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
Bus.send(Mic.K.activityDate, Date() + 7)
}
}
var subscriptions = [AnyCancellable]()
func testMic2_id(_ id: String) {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
Bus.send(Bus.keyId(Mic.K.activityDate, id), Date() + 2)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
Bus.send(Bus.keyId(Mic.K.activityDate, id), Date() + 2)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
Bus.send(Bus.keyId(Mic.K.activityDate, id), Date() + 3)
}
}
}