Wrapping custom class delegates into RxSwift Observables - delegates

I am trying to observe on custom class delegates. I started with
public var didTapAvatar: Observable<()> {
return delegate
.methodInvoked(#selector(JSQMessagesCollectionViewDelegateFlowLayout.collectionView(_:didTapAvatarImageView:at:)))
.map { _ in ()
}
}
which will cause an error as below
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[RxCocoa.RxCollectionViewDelegateProxy collectionView:layout:heightForCellTopLabelAtIndexPath:]: unrecognized selector sent to instance 0x618000298b00
I later tried this
public var jsqdelegate: DelegateProxy {
return RxJSQMessageCollectionViewCellProxy(parentObject: base)
}
public var didTapAvatar: Observable<()> {
return jsqdelegate
.methodInvoked(#selector(JSQMessagesCollectionViewDelegateFlowLayout.collectionView(_:didTapAvatarImageView:at:)))
.map { _ in ()
}
which will succeed on running but will immediately complete and dispose as shown by printing them out onto the console:
self.collectionView.rx.didTapAvatar.asObservable()
.subscribe(onNext: { (event) in
print("next")
}, onError: { (error) in
print("error")
}, onCompleted: {
print("complete")
}, onDisposed: {
print("disposed")
}).disposed(by: disposeBag)
RxJSQMessageCollectionViewCellProxy.swift
public class RxJSQMessageCollectionViewCellProxy: DelegateProxy, JSQMessagesCollectionViewDelegateFlowLayout, DelegateProxyType {
public class func currentDelegateFor(_ object: AnyObject) -> AnyObject? {
let collectionView: JSQMessagesCollectionView = object as! JSQMessagesCollectionView
return collectionView.delegate
}
public class func setCurrentDelegate(_ delegate: AnyObject?, toObject object: AnyObject) {
let collectionView: JSQMessagesCollectionView = object as! JSQMessagesCollectionView
collectionView.delegate = delegate as? JSQMessagesCollectionViewDelegateFlowLayout
}
}
JSQMessagesCollectionView+RxCreate.swift
extension Reactive where Base: JSQMessagesCollectionView {
public var didTapAvatar: Observable<()> {
return delegate.methodInvoked(#selector(JSQMessagesCollectionView.messagesCollectionViewCellDidTapAvatar(_:))).map { _ in () }
}
}

Related

SwiftUI: Sharing NSSharingService on macOS not receiving share

I have a simple test application that attempts to share a CoreData record with another user using NSSharingService. I can create my share and that works,
but when I try to receive the share it opens up the application but doesn't do anything.
I have added CKSharingSupported to my plist.
I have also followed this link to no avail:
CloudKit CKShare userDidAcceptCloudKitShareWith Never Fires on Mac App
Here is my code:
SharingServiceApp:
final class AppDelegate: NSObject, NSApplicationDelegate
{
func application(_ application: NSApplication, userDidAcceptCloudKitShareWith metadata: CKShare.Metadata)
{
print ("userDidAcceptCloudKitShareWith")
let shareStore = persistenceController.sharedPersistentStore!
persistenceController.container.acceptShareInvitations(from: [metadata], into: shareStore)
{ _, error in
if let error = error
{
print("acceptShareInvitation error :\(error)")
}
}
}
}
ContentView:
import SwiftUI
import CoreData
import CloudKit
let persistenceController = PersistenceController()
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
VStack
{
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
Button("Share")
{
shareRecord(item: item)
}
}
} label: {
Text(item.timestamp!, formatter: itemFormatter)
}
}
}
.toolbar {
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
func shareRecord(item: Item)
{
Task
{
if let share = await createShare(item: item)
{
let container = CKContainer(identifier: "iCloud.com.xxxxxxxx.sharingservice")
let item = NSItemProvider()
item.registerCloudKitShare(share, container: container)
DispatchQueue.main.async
{
let sharingService = NSSharingService(named: .cloudSharing)
sharingService!.perform(withItems: [item])
}
}
}
}
private func createShare(item: Item) async -> CKShare?
{
do
{
let (_, share, _) = try await persistenceController.container.share([item], to: nil)
share[CKShare.SystemFieldKey.title] = "MyApp"
return share
}
catch
{
print("Failed to create share")
return nil
}
}
OK I have finally managed to get userDidAcceptCloudKitShareWith to be called.
You need to create the app delegate for your SwiftUI app using #NSApplicationDelegateAdaptor:
#main
struct Sharing_ServiceApp: App
{
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene
{
WindowGroup
{
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
I put that line in and my code instantly started receiving the share requests.

SwiftUI & WidgetKit: Why Intent Handler does not load saved data?

I'm building a Widget with dynamic configuration. I provide a dynamic list of options with an Intents Extension - inside Intent Handler (code below).
However only sampleData shows up, but not the userScrums when tapping on "Edit Widget".
Why it doesn't load userScrums?
import Intents
class IntentHandler: INExtension {
override func handler(for intent: INIntent) -> Any {
// This is the default implementation. If you want different objects to handle different intents,
// you can override this and return the handler you want for that particular intent.
return self
}
}
extension IntentHandler: ScrumSelectionIntentHandling {
func provideScrumOptionsCollection(for intent: ScrumSelectionIntent) async throws -> INObjectCollection<ScrumType> {
let sampleScrums = DailyScrum.sampleData.map { scrum in
ScrumType(identifier: scrum.title, display: scrum.title)
}
let userScrums = try? await ScrumStore.load().map { scrum in
ScrumType(identifier: scrum.title, display: scrum.title)
}
let allScrums = sampleScrums + (userScrums ?? [])
let collection = INObjectCollection(items: allScrums)
return collection
}
// func provideScrumOptionsCollection(for intent: ScrumSelectionIntent, with completion: #escaping (INObjectCollection<ScrumType>?, Error?) -> Void) {}
}
import Foundation
import SwiftUI
class ScrumStore: ObservableObject {
#Published var scrums: [DailyScrum] = []
private static func fileURL() throws -> URL {
try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
.appendingPathComponent("scrums.data")
}
static func load() async throws -> [DailyScrum] {
try await withCheckedThrowingContinuation { continuation in
load { result in
switch result {
case .failure(let error):
continuation.resume(throwing: error)
case .success(let scrums):
continuation.resume(returning: scrums)
}
}
}
}
static func load(completion: #escaping (Result<[DailyScrum], Error>)->Void) {
DispatchQueue.global(qos: .background).async {
do {
let fileURL = try fileURL()
guard let file = try? FileHandle(forReadingFrom: fileURL) else {
DispatchQueue.main.async {
completion(.success([]))
}
return
}
let dailyScrums = try JSONDecoder().decode([DailyScrum].self, from: file.availableData)
DispatchQueue.main.async {
completion(.success(dailyScrums))
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}
#discardableResult
static func save(scrums: [DailyScrum]) async throws -> Int {
try await withCheckedThrowingContinuation { continuation in
save(scrums: scrums) { result in
switch result {
case .failure(let error):
continuation.resume(throwing: error)
case .success(let scrumsSaved):
continuation.resume(returning: scrumsSaved)
}
}
}
}
static func save(scrums: [DailyScrum], completion: #escaping (Result<Int, Error>)->Void) {
DispatchQueue.global(qos: .background).async {
do {
let data = try JSONEncoder().encode(scrums)
let outfile = try fileURL()
try data.write(to: outfile)
DispatchQueue.main.async {
completion(.success(scrums.count))
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}
}

Can't pass the RxSwift PublishRelay value from custom view

I am trying to extend my KeyboardView view with rx action with no success. According to debugging with breakpoints value is passed to the relay but extension is not called despite further subscription in a view controller. What might be a problem and how to fix it?
final class KeyboardView: UIView {
private let disposeBag = DisposeBag()
fileprivate let buttonTappedRelay = PublishRelay<ActionType>()
private let digitButtons: [KeyboardButton] = {
return stride(from: 0, through: 9, by: 1)
.compactMap { $0 }
.map { KeyboardButton(actionType: .digit($0)) }
}()
private let eraseButton: KeyboardButton = {
let button = KeyboardButton(actionType: .erase)
return button
}()
public override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
setupConstraints()
setupActions()
}
private func setupViews() { ... }
private func setupConstraints() { ... }
private func setupActions() {
eraseButton.rx.buttonTap
.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] actionType in
self?.buttonTappedRelay.accept(actionType)
}).disposed(by: self.disposeBag)
for button in digitButtons {
button.rx.buttonTap
.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] actionType in
self?.buttonTappedRelay.accept(actionType)
}).disposed(by: self.disposeBag)
}
}
}
extension Reactive where Base: KeyboardView {
internal var buttonTap: ControlEvent<ActionType> {
return ControlEvent<ActionType>(events: base.buttonTappedRelay.asObservable() )
}
}
Your problem is likely in code you haven't shown. Note that the below code compiles, runs and works:
final class KeyboardView: UIView {
private let disposeBag = DisposeBag()
fileprivate let buttonTappedRelay = PublishRelay<ActionType>()
private let digitButtons: [KeyboardButton] = {
return stride(from: 0, through: 9, by: 1)
.compactMap { $0 }
.map { KeyboardButton(actionType: .digit($0)) }
}()
private let eraseButton: KeyboardButton = {
let button = KeyboardButton(actionType: .erase)
return button
}()
public override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
setupActions()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
let stack = UIStackView(frame: bounds)
stack.distribution = .equalSpacing
stack.addArrangedSubview(eraseButton)
for each in digitButtons {
stack.addArrangedSubview(each)
}
addSubview(stack)
}
private func setupActions() {
eraseButton.rx.buttonTap
.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] actionType in
self?.buttonTappedRelay.accept(actionType)
}).disposed(by: self.disposeBag)
for button in digitButtons {
button.rx.buttonTap
.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] actionType in
self?.buttonTappedRelay.accept(actionType)
}).disposed(by: self.disposeBag)
}
}
}
final class ViewController: UIViewController {
weak var keyboard: KeyboardView?
override func loadView() {
super.loadView()
let keyboard = KeyboardView(frame: view.bounds)
view.addSubview(keyboard)
self.keyboard = keyboard
}
override func viewDidLoad() {
super.viewDidLoad()
keyboard!.rx.buttonTap
.debug("🟣")
.subscribe()
}
}
extension Reactive where Base: KeyboardView {
internal var buttonTap: ControlEvent<ActionType> {
return ControlEvent<ActionType>(events: base.buttonTappedRelay.asObservable() )
}
}
enum ActionType {
case digit(Int)
case erase
}
class KeyboardButton: UIButton {
let actionType: ActionType
init(actionType: ActionType) {
self.actionType = actionType
super.init(frame: CGRect.zero)
backgroundColor = .red
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension Reactive where Base: KeyboardButton {
var buttonTap: Observable<ActionType> {
base.rx.tap.map { base.actionType }
}
}

Start / update View in userContentController - Function

I try to start a second view with the help of the WKWebview by clicking a button.
It also works. I get a callback in the function userContentController. How is it possible to start a new view in the app from this function.
struct MyWebView: UIViewRepresentable {
var webPageURL = "https://google.de"
func updateUIView(_ uiView: WKWebView, context: Context) {
let myURL = URL(string:webPageURL)
let myRequest = URLRequest(url: myURL!)
uiView.load(myRequest)
}
func makeUIView(context: Context) -> WKWebView {
let webConfiguration = WKWebViewConfiguration()
let wkcontentController = WKUserContentController()
wkcontentController.add(context.coordinator, name: "doStuffMessageHandler")
webConfiguration.userContentController = wkcontentController
let webView = WKWebView(frame: .zero,configuration: webConfiguration)
context.coordinator.parent = webView // inject as weak
return webView
}
func makeCoordinator() -> ContentController {
ContentController() // let handler be a coordinator
}
}
class ContentController: NSObject, WKScriptMessageHandler {
weak var parent: WKWebView? // weak to avoid reference cycling
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)
{
if message.name == "doStuffMessageHandler"
{
print(message.body)
}
}
}
struct MyWebView_Previews: PreviewProvider {
static var previews: some View {
MyWebView()
}
}
The possible solution is using notifications (which allows keep those components independent).
extension Notification.Name {
static let didReceiveMessage = Notification.Name("didReceiveMessage")
}
class ContentController: NSObject, WKScriptMessageHandler {
weak var parent: WKWebView? // weak to avoid reference cycling
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)
{
if message.name == "doStuffMessageHandler"
{
NotificationCenter.default.post(name: .didReceiveMessage, object: message.body)
}
}
}
struct DemoHandleMessage: View {
#State private var showMessage = false
#State private var message: String? = nil
var body: some View {
VStack {
if showMessage {
// switch back via bound flag to showMessage state
MessageView(text: message, dismiss: $showMessage)
} else {
MyWebView()
}
}.onReceive(NotificationCenter.default.publisher(for: .didReceiveMessage)) { notification in
self.message = notification.object as? String
self.showMessage = true
}
}
}

Update SwiftUI List after network request using RxSwift

I need to update a SwiftUI List after making a network request. For requests, I use Moya approach with combination of triggers(Input&Output - "Kickstarter").
I cant use Combine framework due to the structure of the project, while they have a lot of helpful advises(not sure about my case).
Simple ContactList:
struct ContactList: View {
var viewModel: UserViewModel
var body: some View {
NavigationView {
List(viewModel.users) { contact in
NavigationLink(destination: ContactDetail(user: contact)) {
ContactRow(user: contact)
}
}
.navigationBarTitle(Text("Team Members"))
}
}
}
Then ViewModel
class UserViewModel {
let disposeBag = DisposeBag()
var users: [TeamMember] = []
init(users: [TeamMember] = []) {
let networkModel = UserNetworkModel()
networkModel.output.teamMembers.subscribe { (event) in
self.users.append(contentsOf: event.element.orEmpty)
}.disposed(by: disposeBag)
networkModel.output.error.subscribe(onNext: { error in
print(error.localizedDescription)
}).disposed(by: disposeBag)
networkModel.input.loadTrigger.onNext(Void())
self.users = users
}
}
And NetworkModel
class UserNetworkModel {
let disposeBag = DisposeBag()
let input: Input
let output: Output
struct Input {
let loadTrigger: AnyObserver<Void>
let searchTrigger: AnyObserver<String>
}
struct Output {
let teamMembers: Observable<[TeamMember]>
let error: Observable<Error>
}
internal let loadSubject = PublishSubject<Void>()
internal let searchSubject = BehaviorSubject<String>(value: "")
internal let errorSubject = PublishSubject<Error>()
internal let teamMembersSubject = BehaviorSubject<[TeamMember]>(value: [])
init() {
let service = MoyaProvider<TeamTarget>()
self.input = Input(loadTrigger: loadSubject.asObserver(), searchTrigger: searchSubject.asObserver())
self.output = Output(teamMembers: teamMembersSubject.asObservable(), error: errorSubject.asObservable())
let result = loadSubject.flatMapLatest { _ -> Observable<[TeamMember]> in
service.rx.request(.get).debug().mapArray(TeamMember.self).asObservable()
}.share(replay: 1)
Observable.combineLatest(result, searchSubject).map { (arg) in
let (members, filter) = arg
if filter.isEmpty {
return members
} else {
let searchText = try! self.searchSubject.value()
return members.filter({
return [$0.firstName, $0.lastName]
.compactMap({ $0 })
.first(where: { $0.hasPrefix(searchText) }) != nil
})
}
}.bind(to: teamMembersSubject).disposed(by: disposeBag)
result.subscribe(onError: { error in
self.errorSubject.onNext(error)
}).disposed(by: self.disposeBag)
}
}
Is it possible to update users array in this way? Or only Combine can do it for me easily?
Thanks for your time.
While I've found some Combine solution. Still waiting for Rx solution.
So, I fixed it just added #ObservedObject for ContactList property and #Published for users' array in Network model. Much less code, natively but not what I was looking for.
Full answer:
struct ContactList: View {
#ObservedObject var networkModel: NetworkModel
var body: some View {
NavigationView {
List(networkModel.users) { contact in
NavigationLink(destination: ContactDetail(user: contact)) {
ContactRow(user: contact)
}
}
.navigationBarTitle(Text("Team Members"))
}
}
}
#if DEBUG
struct ContactList_Previews: PreviewProvider {
static var previews: some View {
ContactList(networkModel: .init(users: contactData))
}
}
#endif
class NetworkModel: ObservableObject {
#Published var users = [User]()
init(users: [User] = []) {
getMembers()
}
private func getMembers() {
let provider = MoyaProvider<TeamTarget>()
provider.request(.get) { [weak self] (result) in
switch result {
case .success(let response):
do {
let result = try JSONDecoder().decode([User].self, from: response.data)
self?.users.append(contentsOf: result)
} catch {
print(error.localizedDescription)
}
case .failure(let error):
print(error.errorDescription.orEmpty)
}
}
}
}

Resources