In Swift, with a class, how to update the connection status? - swift

In order to detect if there is a network connection, I have created a class:
import Foundation
import Network
final class NetworkMonitor: ObservableObject {
static let shared = NetworkMonitor()
private let queue = DispatchQueue(label: "Network") // DispatchQueue.global()
private let monitor: NWPathMonitor
public private(set) var isConnected: Bool = false
public private(set) var connectionType: ConnectionType = .unknown
enum ConnectionType {
case wifi
case cellular
case ethernet
case unknown
}
private init() {
monitor = NWPathMonitor()
}
public func startMonitoring() {
monitor.start(queue: queue)
monitor.pathUpdateHandler = { [weak self] path in
self?.isConnected = path.status == .satisfied
self?.getConnectionType(path)
DispatchQueue.main.async {
self?.objectWillChange.send()
}
}
print("##34355 Etat de la connexion: \(self.isConnected)")
}
public func stopMonitoring() {
monitor.cancel()
}
private func getConnectionType(_ path: NWPath) {
if path.usesInterfaceType(.wifi) {
connectionType = .wifi
} else if path.usesInterfaceType(.cellular) {
connectionType = .cellular
} else if path.usesInterfaceType(.wiredEthernet) {
connectionType = .ethernet
} else {
connectionType = .unknown
}
}
}
I use it like this everywhere in my code in some func after viewDidAppear:
if NetworkMonitor.shared.isConnected == true {
// Do something
}
And I put this in the appDelegate:
NetworkMonitor.shared.startMonitoring()
The connection is detected correctly the first time, but if it changes, the isConnected doesn't work well. he displays the opposite:
When i active the wifi for the simulator, the isConnected is on false, and when i shut it down, he is on true.
I already tried to work with .unsatisfied. but with no success
How could I fix it?

Related

Swift: receive UDP packet using Network framework

I'm studying swift 5.6 Network framework. For this I have a Java-based server, that waits a udp packet of size 64 at localhost port 10000 and sends it back to localhost port 20000. Here is my implementation for Swift :
import Foundation
import Network
class UdpConnection {
private var connection: NWConnection?
private var isConnectionReady = false
init?(host: String, port: UInt16) {
self.connection = NWConnection(
host: NWEndpoint.Host(host),
port: NWEndpoint.Port(integerLiteral: port),
using: .udp
)
let connectionEstablishWaiter = DispatchSemaphore(value: 0)
self.connection?.stateUpdateHandler = { [weak self] (newState) in
switch (newState) {
case .ready:
self?.isConnectionReady = true
default :
self?.isConnectionReady = false
}
connectionEstablishWaiter.signal()
}
self.connection?.start(queue: .global())
switch connectionEstablishWaiter.wait(timeout: .now() + 1) {
case .timedOut:
return nil
default:
()
}
}
func sendUDP(content: Data) {
let sema = DispatchSemaphore(value: 0)
self.connection?.send(content: content, completion: NWConnection.SendCompletion.contentProcessed(({ (NWError) in
if (NWError == nil) {
print("Data was sent to UDP")
} else {
print("ERROR! Error when data (Type: Data) sending. NWError: \n \(NWError!)")
}
sema.signal()
})))
sema.wait()
}
func receiveUDP() {
self.connection?.receiveMessage { (data, context, isComplete, error) in
if (isComplete) {
if let data = data {
print("Receive is complete : \(data.count)")
} else {
print("Data == nil")
}
}
}
}
}
And here is my test app :
import Foundation
if let udpRequestConnection = UdpConnection(host: "127.0.0.1", port: 10_000) {
print("connection established OK")
if let udpResponseConnection = UdpConnection(host: "127.0.0.1", port: 20_000) {
let data = Data(count: 64)
udpResponseConnection.receiveUDP()
udpRequestConnection.sendUDP(content: data)
print("sent")
}
} else {
print("connection establishing FAILURE")
}
I see no packet received and moreover I see a strange picture in Wireshark :
What am I doing wrong? Why is there an ICMP packet ? What am I missing to get this UDP ?
Ok, it seems that I got the misunderstanding -> NWConnection ctor is responsible for setting outbound connection params. So to listen to UDP stream I need the following implementation :
import Foundation
import Network
import Combine
class UDPListener: ObservableObject {
var listener: NWListener?
var connection: NWConnection?
var queue = DispatchQueue.global(qos: .userInitiated)
/// New data will be place in this variable to be received by observers
#Published private(set) public var messageReceived: Data?
/// When there is an active listening NWConnection this will be `true`
#Published private(set) public var isReady: Bool = false
/// Default value `true`, this will become false if the UDPListener ceases listening for any reason
#Published public var listening: Bool = true
/// A convenience init using Int instead of NWEndpoint.Port
convenience init(on port: Int) {
self.init(on: NWEndpoint.Port(integerLiteral: NWEndpoint.Port.IntegerLiteralType(port)))
}
/// Use this init or the one that takes an Int to start the listener
init(on port: NWEndpoint.Port) {
let params = NWParameters.udp
params.allowFastOpen = true
self.listener = try? NWListener(using: params, on: port)
self.listener?.stateUpdateHandler = { update in
switch update {
case .ready:
self.isReady = true
case .failed, .cancelled:
// Announce we are no longer able to listen
self.listening = false
self.isReady = false
default:
()
}
}
self.listener?.newConnectionHandler = { connection in
self.createConnection(connection: connection)
}
self.listener?.start(queue: self.queue)
}
private func createConnection(connection: NWConnection) {
self.connection = connection
self.connection?.stateUpdateHandler = { (newState) in
switch (newState) {
case .ready:
self.receive()
case .cancelled, .failed:
// Cancel the listener, something went wrong
self.listener?.cancel()
// Announce we are no longer able to listen
self.listening = false
default:
()
}
}
self.connection?.start(queue: .global())
}
func receive() {
self.connection?.receiveMessage { data, context, isComplete, error in
if error != nil {
return
}
guard isComplete, let data = data else {
return
}
self.messageReceived = data
print("Rx data size = \(data.count)")
if self.listening {
self.receive()
}
}
}
func cancel() {
self.listening = false
self.connection?.cancel()
}
}
With this I am able to get the UDP response I expect.

How to return to "View Parent" from ASWebAutheticationSession

How to return from the ASWebAutheticationSession completion handler back to the View?
Edit: Just for clearance this is not the original code in my project this is extremely shortened and is just for showcasing what I mean.
Here's an example of a code
struct SignInView: View {
#EnvironmentObject var signedIn: UIState
var body: some View {
let AuthenticationSession = AuthSession()
AuthenticationSession.webAuthSession.presentationContextProvider = AuthenticationSession
AuthenticationSession.webAuthSession.prefersEphemeralWebBrowserSession = true
AuthenticationSession.webAuthSession.start()
}
}
class AuthSession: NSObject, ObservableObject, ASWebAuthenticationPresentationContextProviding {
var webAuthSession = ASWebAuthenticationSession.init(
url: AuthHandler.shared.signInURL()!,
callbackURLScheme: "",
completionHandler: { (callbackURL: URL?, error: Error?) in
// check if any errors appeared
// get code from authentication
// Return to view to move on with code? (return code)
})
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return ASPresentationAnchor()
}
}
So what I'm trying to do is call the sign In process and then get back to the view with the code from the authentication to move on with it.
Could somebody tell me how this may be possible?
Thanks.
Not sure if I'm correctly understanding your question but it is normally done with publishers, commonly with the #Published wrapper, an example:
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
Button {
self.viewModel.signIn(user: "example", password: "example")
}
label: {
Text("Sign in")
}
if self.viewModel.signedIn {
Text("Successfully logged in")
}
else if let error = self.viewModel.signingError {
Text("Error while logging in: \(error.localizedDescription)")
}
}
.padding()
}
}
class ViewModel: ObservableObject {
#Published var signingStatus = SigningStatus.idle
var signedIn: Bool {
if case .success = self.signingStatus { return true }
return false
}
var signingError: Error? {
if case .failure(let error) = self.signingStatus { return error }
return nil
}
func signIn(user: String, password: String) {
self.dummyAsyncProcessWithCompletionHandler { [weak self] success in
guard let self = self else { return }
guard success else {
self.updateSigning(.failure(CustomError(errorDescription: "Login failed")))
return
}
self.updateSigning(.success)
}
}
private func updateSigning(_ status: SigningStatus) {
DispatchQueue.main.async {
self.signingStatus = status
}
}
private func dummyAsyncProcessWithCompletionHandler(completion: #escaping (Bool) -> ()) {
Task {
print("Signing in")
try await Task.sleep(nanoseconds: 500_000_000)
guard Int.random(in: 0..<9) % 2 == 0 else {
print("Error")
completion(false)
return
}
print("Success")
completion(true)
}
}
enum SigningStatus {
case idle
case success
case failure(Error)
}
struct CustomError: Error, LocalizedError {
var errorDescription: String?
}
}

SwiftUI app freezing when using multiple product identifiers in StoreKit

I'm currently learning Swift and following some tutorials but I'm stuck on a StoreKit issue.
The code works when I provide a single productIdentifier, but when I provide more than 1 in the Set, the entire app hangs on loading. This is in the iOS Simulator, and on a device. I've got 2 identifiers in the set, and both of these work individually, but not at the same time. My code looks the same as the original tutorial (video) so I don't know where I'm going long.
Entire Store.swift file below. Problem appears to be in the fetchProducts function, but I'm not sure. Can anyone point me in the right direction?
import StoreKit
typealias FetchCompletionHandler = (([SKProduct]) -> Void)
typealias PurchaseCompletionHandler = ((SKPaymentTransaction?) -> Void)
class Store: NSObject, ObservableObject {
#Published var allRecipes = [Recipe]() {
didSet {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
for index in self.allRecipes.indices {
self.allRecipes[index].isLocked = !self.completedPurchases.contains(self.allRecipes[index].id)
}
}
}
}
private let allProductIdentifiers = Set(["com.myname.ReceipeStore.test", "com.myname.ReceipeStore.test2"])
private var completedPurchases = [String]()
private var productsRequest: SKProductsRequest?
private var fetchedProducts = [SKProduct]()
private var fetchCompletionHandler: FetchCompletionHandler?
private var purchaseCompletionHandler: PurchaseCompletionHandler?
override init() {
super.init()
startObservingPaymentQueue()
fetchProducts { products in
self.allRecipes = products.map { Recipe(product: $0) }
}
}
private func startObservingPaymentQueue() {
SKPaymentQueue.default().add(self)
}
private func fetchProducts(_ completion: #escaping FetchCompletionHandler) {
guard self.productsRequest == nil else { return }
fetchCompletionHandler = completion
productsRequest = SKProductsRequest(productIdentifiers: allProductIdentifiers)
productsRequest!.delegate = self
productsRequest!.start()
}
private func buy(_ product: SKProduct, competion: #escaping PurchaseCompletionHandler) {
purchaseCompletionHandler = competion
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
}
extension Store {
func product(for identififier: String) -> SKProduct? {
return fetchedProducts.first(where: { $0.productIdentifier == identififier })
}
func purchaseProduct(_ product: SKProduct) {
buy(product) { _ in }
}
}
extension Store: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
var shouldFinishTransactions = false
switch transaction.transactionState {
case .purchased, .restored:
completedPurchases.append(transaction.payment.productIdentifier)
shouldFinishTransactions = true
case .failed:
shouldFinishTransactions = true
case .deferred, .purchasing:
break
#unknown default:
break
}
if shouldFinishTransactions {
SKPaymentQueue.default().finishTransaction(transaction)
DispatchQueue.main.async {
self.purchaseCompletionHandler?(transaction)
self.purchaseCompletionHandler = nil
}
}
}
}
}
// loading products from the store
extension Store: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
let loadedProducts = response.products
let invalidProducts = response.invalidProductIdentifiers
guard !loadedProducts.isEmpty else {
print("Could not load the products!")
if !invalidProducts.isEmpty {
print("Invalid products found: \(invalidProducts)")
}
productsRequest = nil
return
}
// cache the feteched products
fetchedProducts = loadedProducts
// notify anyone waiting on the product load (swift UI view)
DispatchQueue.main.async {
self.fetchCompletionHandler?(loadedProducts)
self.fetchCompletionHandler = nil
self.productsRequest = nil
}
}
}```
It looks like you're running all of your requests on the main DispatchQueue, this will block other main queue work until completed. You should consider handling some of these tasks with a custom concurrent queue. This bit of sample code should get the ball rolling.
func requestProducts(_ productIdentifiers: Set<ProductIdentifier>, handler: #escaping ProductRequestHandler) {
// Set request handler
productRequest?.cancel()
productRequestHandler = handler
// Request
productRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
productRequest?.delegate = self
productRequest?.start()
}
func requestPrices() {
// Retry interval, 5 seconds, set this to your liking
let retryTimeOut = 5.0
var local1: String? = nil
var local2: String? = nil
let bundleIdentifier = Bundle.main.bundleIdentifier!
let queue = DispatchQueue(label: bundleIdentifier + ".IAPQueue", attributes: .concurrent)
// Request price
queue.async {
var trying = true
while(trying) {
let semaphore = DispatchSemaphore(value: 0)
requestProducts(Set(arrayLiteral: SettingsViewController.pID_1000Credits, SettingsViewController.pID_2000Credits)) { (response, error) in
local1 = response?.products[0].localizedPrice
local2 = response?.products[1].localizedPrice
semaphore.signal()
}
// We will keep checking on this thread until completed
_ = semaphore.wait(timeout: .now() + retryTimeOut)
if(local2 != nil) { trying = false }
}
// Update with main thread once request is completed
DispatchQueue.main.async {
self.price1 = local1 ?? "$0.99"
self.price2 = local2 ?? "$1.99"
}
}
}
extension SKProduct {
// Helper function, not needed for this example
public var localizedPrice: String? {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = self.priceLocale
return formatter.string(from: self.price)
}

Swift: Notification permissions pop up affecting class functionality

This is a hard one to explain as I'm not entirely sure what's going wrong here. In my AppDelegate I add an array of services to the app:
override var services: [ApplicationService] {
if UIApplication.isRunningUnitTests() {
return []
}
return [
SettingsService(),
TrackingApplicationService(),
NotificationApplicationService(),
AudioApplicationService(),
ConnectionService()
]
}
The connection service is the one I am interested in here. It looks like this:
import PluggableAppDelegate
import Connectivity
final class ConnectionService: NSObject, ApplicationService {
// MARK: - Helpers
private struct Constants {
static let containerHeight: CGFloat = 20
static let containerPadding: CGFloat = 20
static let statusCenterAdjustment: CGFloat = -1
static let statusFont: UIFont = .systemFont(ofSize: 15, weight: .semibold)
static let labelLines = 1
static let radiusDenominator: CGFloat = 2.0
static let animationDuration = 0.2
static let defaultAnchorConstant: CGFloat = 0
}
internal enum ConnectionStatus {
case connected
case disconnected
}
// MARK: - Properties
private let connectivity = Connectivity()
private let queue = DispatchQueue.global(qos: .utility)
internal var currentStatus = ConnectionStatus.connected
private var statusTopAnchor: NSLayoutConstraint?
private lazy var statusContainer: UIView = {
let container = UIView()
container.translatesAutoresizingMaskIntoConstraints = false
container.backgroundColor = UIColor.red
container.layer.cornerRadius = Constants.containerHeight / Constants.radiusDenominator
container.layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMinXMaxYCorner]
container.isHidden = true
container.addSubview(statusLabel)
NSLayoutConstraint.activate([
statusLabel.centerYAnchor.constraint(equalTo: container.centerYAnchor, constant: Constants.statusCenterAdjustment),
statusLabel.leftAnchor.constraint(equalTo: container.leftAnchor, constant: Constants.containerPadding),
statusLabel.rightAnchor.constraint(equalTo: container.rightAnchor, constant: -Constants.containerPadding)
])
return container
}()
private lazy var statusLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = Constants.labelLines
label.font = Constants.statusFont
label.textAlignment = .center
label.textColor = .white
label.text = Strings.common.user.noInternetConnection.localized
return label
}()
// MARK: - Lifecycle
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
DispatchQueue.main.async { [weak self] in
self?.prepareUI()
}
prepareConnectivityListener()
updateConnectionStatus(connectivity.status)
return true
}
// MARK: - Connectivity
func updateConnectionStatus(_ status: Connectivity.Status) {
switch status {
case .connected, .connectedViaWiFi, .connectedViaCellular:
updateLabelWith(connectionStatus: .connected)
case .notConnected, .connectedViaWiFiWithoutInternet, .connectedViaCellularWithoutInternet:
updateLabelWith(connectionStatus: .disconnected)
}
NotificationCenter.default.post(name: .connectivityStatusChanged, object: self.currentStatus)
}
// MARK: - Configuration
private func prepareUI() {
guard let window = UIApplication.shared.keyWindow else {
assertionFailure("We require a Window")
return // Don't setup in release if we hit this, no point
}
window.addSubview(statusContainer)
statusTopAnchor = statusContainer.topAnchor.constraint(equalTo: window.topAnchor, constant: -Constants.containerHeight)
guard let statusTopAnchor = statusTopAnchor else {
assertionFailure("Top anchor could not be build")
return
}
NSLayoutConstraint.activate([
statusTopAnchor,
statusContainer.centerXAnchor.constraint(equalTo: window.centerXAnchor),
statusContainer.heightAnchor.constraint(equalToConstant: Constants.containerHeight)
])
}
private func prepareConnectivityListener() {
let connectivityChanged: (Connectivity) -> Void = { [weak self] connectivity in
self?.updateConnectionStatus(connectivity.status)
}
connectivity.whenConnected = connectivityChanged
connectivity.whenDisconnected = connectivityChanged
connectivity.startNotifier(queue: queue)
}
private func updateLabelWith(connectionStatus: ConnectionStatus) {
if currentStatus == connectionStatus {
return // No need to update if we are the same state
} else {
currentStatus = connectionStatus
}
let topAnchorConstant: CGFloat = (connectionStatus == .connected) ? -Constants.containerHeight : Constants.defaultAnchorConstant
DispatchQueue.main.async { [weak self] in
DispatchQueue.main.async { [weak self] in
if connectionStatus != .connected {
self?.statusContainer.isHidden = false
}
self?.statusTopAnchor?.constant = topAnchorConstant
UIView.animate(withDuration: Constants.animationDuration,
animations: { [weak self] in
self?.statusContainer.superview?.layoutIfNeeded()
},
completion: { _ in
if connectionStatus == .connected {
self?.statusContainer.isHidden = true
}
})
}
}
}
}
So I'm using this service to check the connection status of the device and on the login view controller I display different warnings depending on the connection status.
However, my problem is is that the connection service is not functioning properly when the permissions pop up appears. As you can see from the ConnectionService class, an alert should be generated when the app is offline. Now because I am adding this connection service before the permissions are requested, the window briefly shows the alert, but as soon as the permissions alert appears, it disappears.
Furthermore, you can see in ConnectionService.swift I have a listener set up:
NotificationCenter.default.post(name: .connectivityStatusChanged, object: self.currentStatus)
I'm listening for this in the subsequent viewController which triggers this method:
#objc private func connectivityChanged(notification: Notification) {
if notification.object as? ConnectionService.ConnectionStatus == .connected {
deviceConnected = true
} else {
deviceConnected = false
}
}
This in turn triggers a UI change:
private var deviceConnected: Bool = true {
willSet {
DispatchQueue.main.async {
if newValue == true {
self.tabBarItem.image = #imageLiteral(resourceName: "retrieve_completed")
self.errorLabel?.isHidden = true
} else {
self.tabBarItem.image = #imageLiteral(resourceName: "retrieve_offline")
self.errorLabel?.isHidden = false
self.errorLabel?.text = Strings.eHandshake.warning.deviceOffline.title.localized
}
}
}
}
When permissions have already been granted and therefore the app launches without requesting permission, this works fine, but when the permissions alert appears, this seems to break this part of the logic.
To be clear the services var is initially declared here:
public protocol ApplicationService: UIApplicationDelegate {}
extension ApplicationService {
public var window: UIWindow? {
return UIApplication.shared.delegate?.window ?? nil
}
}
open class PluggableApplicationDelegate: UIResponder,
UIApplicationDelegate {
public var window: UIWindow?
open var services: [ApplicationService] { return [] }
internal lazy var _services: [ApplicationService] = {
return self.services
}()
#discardableResult
internal func apply<T, S>(_ work: (ApplicationService, #escaping (T) -> Void) -> S?, completionHandler: #escaping ([T]) -> Swift.Void) -> [S] {
let dispatchGroup = DispatchGroup()
var results: [T] = []
var returns: [S] = []
for service in _services {
dispatchGroup.enter()
let returned = work(service, { result in
results.append(result)
dispatchGroup.leave()
})
if let returned = returned {
returns.append(returned)
} else { // delegate doesn't implement method
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
completionHandler(results)
}
return returns
}
}
Any help with this would be much appreciated.

Custom Property Wrapper that Updates View Swift

Xcode 11.3, Swift 5.1.3
I am trying currently to create a custom property wrapper that allows me to link variables to a Firebase database. When doing this, to make it update the view, I at first tried to use the #ObservedObject #Bar var foo = []. But I get an error that multiple property wrappers are not supported. Next thing I tried to do, which would honestly be ideal, was try to make my custom property wrapper update the view itself upon being changed, just like #State and #ObservedObject. This both avoids needing to go down two layers to access the underlying values and avoid the use of nesting property wrappers. To do this, I checked the SwiftUI documentation and found out that they both implement the DynamicProperty protocol. I tried to use this too but failed because I need to be able to update the view (call update()) from within my Firebase database observers, which I cannot do since .update() is mutating.
Here is my current attempt at this:
import SwiftUI
import Firebase
import CodableFirebase
import Combine
#propertyWrapper
final class DatabaseBackedArray<Element>: ObservableObject where Element: Codable & Identifiable {
typealias ObserverHandle = UInt
typealias Action = RealtimeDatabase.Action
typealias Event = RealtimeDatabase.Event
private(set) var reference: DatabaseReference
private var currentValue: [Element]
private var childAddedObserverHandle: ObserverHandle?
private var childChangedObserverHandle: ObserverHandle?
private var childRemovedObserverHandle: ObserverHandle?
private var childAddedActions: [Action<[Element]>] = []
private var childChangedActions: [Action<[Element]>] = []
private var childRemovedActions: [Action<[Element]>] = []
init(wrappedValue: [Element], _ path: KeyPath<RealtimeDatabase, RealtimeDatabase>, events: Event = .all,
actions: [Action<[Element]>] = []) {
currentValue = wrappedValue
reference = RealtimeDatabase()[keyPath: path].reference
for action in actions {
if action.event.contains(.childAdded) {
childAddedActions.append(action)
}
if action.event.contains(.childChanged) {
childChangedActions.append(action)
}
if action.event.contains(.childRemoved) {
childRemovedActions.append(action)
}
}
if events.contains(.childAdded) {
childAddedObserverHandle = reference.observe(.childAdded) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.append(decodedValue)
self.childAddedActions.forEach { $0.action(&self.currentValue) }
}
}
if events.contains(.childChanged) {
childChangedObserverHandle = reference.observe(.childChanged) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
guard let changeIndex = self.currentValue.firstIndex(where: { $0.id == decodedValue.id }) else {
return
}
self.objectWillChange.send()
self.currentValue[changeIndex] = decodedValue
self.childChangedActions.forEach { $0.action(&self.currentValue) }
}
}
if events.contains(.childRemoved) {
childRemovedObserverHandle = reference.observe(.childRemoved) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.removeAll { $0.id == decodedValue.id }
self.childRemovedActions.forEach { $0.action(&self.currentValue) }
}
}
}
private func setValue(to value: [Element]) {
guard let encodedValue = try? FirebaseEncoder().encode(currentValue) else {
fatalError("Could not encode value to Firebase.")
}
reference.setValue(encodedValue)
}
var wrappedValue: [Element] {
get {
return currentValue
}
set {
self.objectWillChange.send()
setValue(to: newValue)
}
}
var projectedValue: Binding<[Element]> {
return Binding(get: {
return self.wrappedValue
}) { newValue in
self.wrappedValue = newValue
}
}
var hasActiveObserver: Bool {
return childAddedObserverHandle != nil || childChangedObserverHandle != nil || childRemovedObserverHandle != nil
}
var hasChildAddedObserver: Bool {
return childAddedObserverHandle != nil
}
var hasChildChangedObserver: Bool {
return childChangedObserverHandle != nil
}
var hasChildRemovedObserver: Bool {
return childRemovedObserverHandle != nil
}
func connectObservers(for event: Event) {
if event.contains(.childAdded) && childAddedObserverHandle == nil {
childAddedObserverHandle = reference.observe(.childAdded) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.append(decodedValue)
self.childAddedActions.forEach { $0.action(&self.currentValue) }
}
}
if event.contains(.childChanged) && childChangedObserverHandle == nil {
childChangedObserverHandle = reference.observe(.childChanged) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
guard let changeIndex = self.currentValue.firstIndex(where: { $0.id == decodedValue.id }) else {
return
}
self.objectWillChange.send()
self.currentValue[changeIndex] = decodedValue
self.childChangedActions.forEach { $0.action(&self.currentValue) }
}
}
if event.contains(.childRemoved) && childRemovedObserverHandle == nil {
childRemovedObserverHandle = reference.observe(.childRemoved) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.removeAll { $0.id == decodedValue.id }
self.childRemovedActions.forEach { $0.action(&self.currentValue) }
}
}
}
func removeObserver(for event: Event) {
if event.contains(.childAdded), let handle = childAddedObserverHandle {
reference.removeObserver(withHandle: handle)
self.childAddedObserverHandle = nil
}
if event.contains(.childChanged), let handle = childChangedObserverHandle {
reference.removeObserver(withHandle: handle)
self.childChangedObserverHandle = nil
}
if event.contains(.childRemoved), let handle = childRemovedObserverHandle {
reference.removeObserver(withHandle: handle)
self.childRemovedObserverHandle = nil
}
}
func removeAction(_ action: Action<[Element]>) {
if action.event.contains(.childAdded) {
childAddedActions.removeAll { $0.id == action.id }
}
if action.event.contains(.childChanged) {
childChangedActions.removeAll { $0.id == action.id }
}
if action.event.contains(.childRemoved) {
childRemovedActions.removeAll { $0.id == action.id }
}
}
func removeAllActions(for event: Event) {
if event.contains(.childAdded) {
childAddedActions = []
}
if event.contains(.childChanged) {
childChangedActions = []
}
if event.contains(.childRemoved) {
childRemovedActions = []
}
}
}
struct School: Codable, Identifiable {
/// The unique id of the school.
var id: String
/// The name of the school.
var name: String
/// The city of the school.
var city: String
/// The province of the school.
var province: String
/// Email domains for student emails from the school.
var domains: [String]
}
#dynamicMemberLookup
struct RealtimeDatabase {
private var path: [String]
var reference: DatabaseReference {
var ref = Database.database().reference()
for component in path {
ref = ref.child(component)
}
return ref
}
init(previous: Self? = nil, child: String? = nil) {
if let previous = previous {
path = previous.path
} else {
path = []
}
if let child = child {
path.append(child)
}
}
static subscript(dynamicMember member: String) -> Self {
return Self(child: member)
}
subscript(dynamicMember member: String) -> Self {
return Self(child: member)
}
static subscript(dynamicMember keyPath: KeyPath<Self, Self>) -> Self {
return Self()[keyPath: keyPath]
}
static let reference = Database.database().reference()
struct Event: OptionSet, Hashable {
let rawValue: UInt
static let childAdded = Event(rawValue: 1 << 0)
static let childChanged = Event(rawValue: 1 << 1)
static let childRemoved = Event(rawValue: 1 << 2)
static let all: Event = [.childAdded, .childChanged, .childRemoved]
static let constructive: Event = [.childAdded, .childChanged]
static let destructive: Event = .childRemoved
}
struct Action<Value>: Identifiable {
let id = UUID()
let event: Event
let action: (inout Value) -> Void
private init(on event: Event, perform action: #escaping (inout Value) -> Void) {
self.event = event
self.action = action
}
static func on<Value>(_ event: RealtimeDatabase.Event, perform action: #escaping (inout Value) -> Void) -> Action<Value> {
return Action<Value>(on: event, perform: action)
}
}
}
Usage example:
struct ContentView: View {
#DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { $0.sort { $0.name < $1.name } }])
var schools: [School] = []
var body: some View {
Text("School: ").bold() +
Text(schools.isEmpty ? "Loading..." : schools.first!.name)
}
}
When I try to use this though, the view never updates with the value from Firebase even though I am positive that the .childAdded observer is being called.
One of my attempts at fixing this was to store all of these variables in a singleton that itself conforms to ObservableObject. This solution is also ideal as it allows the variables being observed to be shared throughout my application, preventing multiples instances of the same date and allowing for a single source of truth. Unfortunately, this too did not update the view with the fetched value of currentValue.
class Session: ObservableObject {
#DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { $0.sort { $0.name < $1.name } }])
var schools: [School] = []
private init() {
//Send `objectWillChange` when `schools` property changes
_schools.objectWillChange.sink {
self.objectWillChange.send()
}
}
static let current = Session()
}
struct ContentView: View {
#ObservedObject
var session = Session.current
var body: some View {
Text("School: ").bold() +
Text(session.schools.isEmpty ? "Loading..." : session.schools.first!.name)
}
}
Is there any way to make a custom property wrapper that also updates a view in SwiftUI?
Making use of the DynamicProperty protocol we can easily trigger view updates by making use of SwiftUI's existing property wrappers. (DynamicProperty tells SwiftUI to look for these within our type)
#propertyWrapper
struct OurPropertyWrapper: DynamicProperty {
// A state object that we notify of updates
#StateObject private var updater = Updater()
var wrappedValue: T {
get {
// Your getter code here
}
nonmutating set {
// Tell SwiftUI we're going to change something
updater.notifyUpdate()
// Your setter code here
}
}
class Updater: ObservableObject {
func notifyUpdate() {
objectWillChange.send()
}
}
}
The solution to this is to make a minor tweak to the solution of the singleton. Credits to #user1046037 for pointing this out to me. The problem with the singleton fix mentioned in the original post, is that it does not retain the canceller for the sink in the initializer. Here is the correct code:
class Session: ObservableObject {
#DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { $0.sort { $0.name < $1.name } }])
var schools: [School] = []
private var cancellers = [AnyCancellable]()
private init() {
_schools.objectWillChange.sink {
self.objectWillChange.send()
}.assign(to: &cancellers)
}
static let current = Session()
}