I took this piece of code from an example of creating a WebSocket connection with the Starscream library based on UIKit. How can I make it work in my SwiftUI project?
class ViewController: UIViewController, WebSocketDelegate {
var socket: WebSocket!
var isConnected: Bool = false
let server = WebSocketServer()
var token: String = ""
override func viewDidLoad() {
super.viewDidLoad()
var request = URLRequest(url: URL(string: "wss://-----")!)
request.timeoutInterval = 5
request.setValue("https://-----", forHTTPHeaderField: "Origin")
socket = WebSocket(request: request)
socket.delegate = self
socket.connect()
}
func didReceive(event: WebSocketEvent, client: WebSocket) {
switch event {
case .connected(let headers):
isConnected = true
print("websocket is connected: (headers)")
case .disconnected(let reason, let code):
isConnected = false
print("websocket is disconnected: (reason) with code: (code)")
case .text(let string):
if string.contains("token expiring") { print("expiring"); socket.disconnect() }
print("Received text: (string)")
case .binary(let data):
print("Received data: (data.count)")
case .ping(_):
break
case .pong(_):
break
case .viabilityChanged(_):
break
case .reconnectSuggested(_):
break
case .cancelled:
isConnected = false
case .error(let error):
isConnected = false
handleError(error)
} }
I would recommend wrapping the delegate in an ObservableObject class:
class WebSocket: NSObject, WebSocketDelegate, ObservableObject {
var socket: WebSocket!
#Published var event: WebSocketEvent?
var isConnected: Bool = false
let server = WebSocketServer()
var token: String = ""
func didReceive(event: WebSocketEvent, client: WebSocket) {
self.event = event
}
func connect(request: URLRequest) {
socket = WebSocket(request: request)
socket.delegate = self
socket.connect()
}
}
And then in your View:
struct YourView: View {
#StateObject var socketDelegate = WebSocket()
var body: some View {
//your view
.onAppear {
var request = URLRequest(url: URL(string: "wss://-----")!)
request.timeoutInterval = 5
request.setValue("https://-----", forHTTPHeaderField: "Origin")
socketDelegate.connect(request: request)
}.onChange(of: socketDelegate.event) { event in
switch event {
case .connected(let headers):
isConnected = true
print("websocket is connected: (headers)")
case .disconnected(let reason, let code):
isConnected = false
print("websocket is disconnected: (reason) with code: (code)")
case .text(let string):
if string.contains("token expiring") { print("expiring"); socket.disconnect() }
print("Received text: (string)")
case .binary(let data):
print("Received data: (data.count)")
case .ping(_):
break
case .pong(_):
break
case .viabilityChanged(_):
break
case .reconnectSuggested(_):
break
case .cancelled:
isConnected = false
case .error(let error):
isConnected = false
handleError(error)
}
}
}
}
Related
Code from tutorial, want to access contacts from my app, and using this as base
Need to solve this error to move forward and do not know what additional items needs initialization. Any help very appreciated.
import Contacts
final class ContactsViewModel: ObservableObject {
#Published
var contact: [Contact?]
#Published
var contacts: [Contact?] = []
#Published
var permissionsError: PermissionsError? = .none
init( contact: Contact, contacts: [Contact] , permissionsError: PermissionsError) {
permissions()
}. **<-- Error occurs here -**
func openSettings() {
permissionsError = .none
guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else {return}
if UIApplication.shared.canOpenURL(settingsURL) { UIApplication.shared.open(settingsURL)}
}
func getContacts() {
Contact.fetchAll { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let fetchedContacts):
DispatchQueue.main.async {
self.contacts = fetchedContacts.sorted(by: { $0.lastName < $1.lastName })
}
case .failure(let error):
self.permissionsError = .fetchError(error)
}
}
}
func permissions() {
switch CNContactStore.authorizationStatus(for: .contacts) {
case.authorized:
getContacts()
case .notDetermined, .restricted, .denied :
CNContactStore().requestAccess(for: .contacts) { [weak self] granted, error in
switch granted {
case true: self?.getContacts()
case false:
DispatchQueue.main.async {
self?.permissionsError = .userError
}
}
}
I have RestManager class which is used to fetch data from Internet
class RestManager {
func fetchData<T: Decodable>(url: URL) -> AnyPublisher<T, ErrorType> {
URLSession.shared
.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: T.self, decoder: JSONDecoder())
.mapError({ error in
if let error = error as? URLError {
switch error.code {
case .notConnectedToInternet, .timedOut, .networkConnectionLost:
return .noInternetConnection
case .cannotDecodeRawData, .cannotDecodeContentData:
return .empty
default:
return .general
}
}
return .general
})
.eraseToAnyPublisher()
}
}
In Repository class there is function getCountriesList which, using RestManager is returning AnyPublisher<[Country], ErrorType> where ErrorType represents enum with custom cases for error handling (.noInternetConnection, .general, .empty)
class Covid19RepositoryImpl: Covid19Repository {
func getCountriesList() -> AnyPublisher<[Country], ErrorType> {
let url = RestEndpoints.countriesList.endpoint()
return RestManager().fetchData(url: url)
}
}
In viewModel class, in function getAllCountries, pipeline is created for fetching and saving data in countries variable, and in .sink in completion I tried to save ErrorType (if there is any error) in specific variable called error
I tried to use like this
class CountriesViewModelImpl: CountriesViewModel {
var repository: Covid19Repository
#Published var countries: [Country] = []
#Published var error: ErrorType?
#Published var loader: Bool = true
private var cancellables: Set<AnyCancellable> = .init()
init(repository: Covid19Repository){
self.repository = repository
getAllCountries()
}
func getAllCountries() {
repository
.getCountriesList()
.receive(on: RunLoop.main)
.sink { error in
self.error = error
}
} receiveValue: { [unowned self] newCountries in
self.countries = newCountries
self.error = nil
self.loader = false
}
.store(in: &cancellables)
}
}
But it returns Cannot assign value of type 'Subscribers.Completion' to type 'ErrorType'
Is there any other way I can handle error?
sink returns an enum in the receiveCompletion closure with finished and failure cases
.sink { completion in
switch completion {
case .finished: print("finished")
case .failure(let error): self.error = error
}
} receiveValue: ...
Here is my requirement: I want to display a login screen, perform authentication via a network request, and if successful, display the content page.
I have implemented the network request part as per the suggestion by jpdnx here.
I have a UserAuth class and the corresponding code to display the Login View if not logged in, and the Content View if logged in, as per M Reza here.
However, I am not able to combine the two - when the network request finishes in LoginView, I am not able to get the flow back to the LoginControllerView to navigate to ContentView. Any help will be appreciate here.
Here is my code:
Network.swift
class ViewModel<T: Codable> : ObservableObject {
//#Published var calendar : IDCCalendar?
#Published var modelData : T?
func getData(url: URL, encoded: String, completion: (#escaping ()->()) ) {
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
print(encoded)
let encoder = JSONEncoder()
if let data = try? encoder.encode(encoded) {
request.httpBody = data
}
print("Request: \(request)")
URLSession.shared.dataTask(with: request) { data1, response, error in
if let error = error {
print("Request error: ", error)
return
}
guard let data1 = data1 else { return }
DispatchQueue.main.async {
let decoder = JSONDecoder()
print(String(decoding: data1, as: UTF8.self))
if let value = try? decoder.decode(T.self, from: data1)
{
self.modelData = value
completion()
print("Success!")
}
else {
print("Does not decode correctly")
}
}
}.resume()
}
LoginControllerView.swift
var body: some View {
Group {
if !userAuth.isLoggedin {
LoginView(userAuth: userAuth)
} else {
ContentView()
}
}
LoginView.swift
struct LoginView: View {
#EnvironmentObject var userAuth: UserAuth
#StateObject var viewModel = ViewModel<Login>()
var body: some View {
// Login Screen UI here
Button("Login") {
// Commenting out network request code out for now
/*
let url = URL(<URL goes here>)!
let encoded = <JSON String goes here>
let completion = {
if viewModel.modelData?.Status == 0 {
userAuth.isLoggedin = true
let login = viewModel.modelData!
let encoder = JSONEncoder()
if let data = try? encoder.encode(login) {
UserDefaults.standard.set(data, forKey: "LoginData")
}
}
viewModel.getData(url: url, encoded: encoded, completion: completion)
*/
// For test purposes, setting it to true, I want this to percolate back to
// LoginControllerView
userAuth.isLoggedin = true
}
UserAuth.swift
import Combine
class UserAuth: ObservableObject {
let didChange = PassthroughSubject<UserAuth,Never>()
// required to conform to protocol 'ObservableObject'
let willChange = PassthroughSubject<UserAuth,Never>()
func login() {
// login request... on success:
self.isLoggedin = true
}
var isLoggedin = false {
didSet {
didChange.send(self)
}
}
}
I am trying to integrate the ORSSerialPort framework with Combine into SwiftUi. ORSSerialPort GitHub
ORSSerialPort implements Key-Value Observing(KVO) and a Delegate Pattern. In his example, he works with the Delegate Pattern and UIKit. I will use Combine and SwiftUI. Not the Delegate Pattern.
Because the ORSSerialPort is a KVO Object, it should be possible ORSSerialPort variables to subscribe, but her is my Problem. It will not work. I don't know why.
what i try:
#Published var isOpen: String = ""
var orsSerialPort: ORSSerialPort = ORSSerialPort(path: "/dev/cu.usbmodem146101")!
var publisher: NSObject.KeyValueObservingPublisher<ORSSerialPort, Bool>
var isOpenSub: AnyCancellable?
init(){
publisher = orsSerialPort.publisher(for: \.isOpen)
isOpenSub = publisher
.map{ (x) -> String in return "Is \(x ? "Opend":"Close" )" }
.assign(to: \.isOpen, on: self)
}
The publisher is trigger always one time, and that is by the init. If I close and open the port with my function, it will not trigger again.
func cloes(){
orsSerialPort.close()
}
func open(){
orsSerialPort.open()
}
For Debugging I try this to the the Stream. And I will see again that the Publisher will always trigger only one time. But the Port is closing and opining.
.sink(receiveCompletion: { completion in
print("subScribeIsOpen complet")
switch completion {
case .finished:
print("subScribeIsOpen complet")
case .failure(let error):
print("subScribeIsOpen fail")
print(error.localizedDescription)
}
}, receiveValue: { value in
print("subScribeIsOpen receive \(value)")
self.isOpen = value
})
My Demo Code
import Foundation
import ORSSerial
import Combine
import SwiftUI
class ORSSerialPortCombine: ObservableObject {
#Published var status: String = ""
#Published var isOpen: String = ""
var orsSerialPort: ORSSerialPort = ORSSerialPort(path: "/dev/cu.usbmodem146101")!
var publisher: NSObject.KeyValueObservingPublisher<ORSSerialPort, Bool>
var isOpenSub: AnyCancellable?
init(){
publisher = orsSerialPort.publisher(for: \.isOpen)
isOpenSub = publisher
.map{ (x) -> String in return "Is \(x ? "Opend":"Close" )" }
// .assign(to: \.isOpen, on: self)
.sink(receiveCompletion: { completion in
print("subScribeIsOpen complet")
switch completion {
case .finished:
print("subScribeIsOpen complet")
case .failure(let error):
print("subScribeIsOpen fail")
print(error.localizedDescription)
}
}, receiveValue: { value in
print("subScribeIsOpen receive \(value)")
self.isOpen = value
})
}
func cloes(){
orsSerialPort.close()
}
func open(){
orsSerialPort.open()
}
func updateStatus(){
status = "Serial port is \(orsSerialPort.isOpen ? "Opend":"Close" )"
}
}
struct ORSSerialPortCombineView: View{
#ObservedObject var model = ORSSerialPortCombine()
var body: some View{
VStack{
Text( model.isOpen )
HStack{
Button("Close") { model.cloes() }
Button("Open") { model.open() }
Button("Staus") { model.updateStatus() }
}
Text( model.status )
}
}
}
I am new to Swiftui and I struggle to understand how to properly retain data created in ObservableObject when rendering views? Or a completely different approach to the problem maybe?
More specifically, it is about getting HTTP data in each row in a List().
Right now, it makes the HTTP call far too often when parent views are rendered, which causes all rows to be reloaded.
The same issue can be found here: Keep reference on view/data model after View update
public class VideoFetcher: ObservableObject {
#Published var video: VideoResponse?
#Published var coverImage: UIImage?
#Published var coverImageLoading = false
#Published var categories: String?
#Published var loading = false
#Published var error = false
func load(mediaItemSlug: String = "", broadcasterSlug: String = "") {
self.loading = true
Video.findBySlug(
mediaItemSlug: mediaItemSlug,
broadcasterSlug: broadcasterSlug,
successCallback: {video -> Void in
self.video = video
self.loading = false
self.setCategories()
self.loadCoverImage()
},
errorCallback: {(error, _) -> Void in
self.loading = false
self.error = true
})
}
func loadCoverImage() {
guard self.video!.coverImageUrl != "" else {
return
}
self.coverImageLoading = true
let downloader = ImageDownloader()
let urlRequest = URLRequest(url: URL(string: self.video!.coverImageUrl)!)
let filter = AspectScaledToFillSizeFilter(size: CGSize(width: 520.0, height: 292.499999963))
downloader.download(urlRequest, filter: filter) { response in
if case .success(let image) = response.result {
self.coverImage = image
self.coverImageLoading = false
}
}
}
func setCategories() {
if (self.video!.broadcaster.categories.count > 0) {
let categoryNames = self.video!.broadcaster.categories.map { category in
return category.name == "" ? "(no name)" : category.name
}
self.categories = categoryNames.joined(separator: " • ");
}
}
}
List() row:
struct VideoCard: View {
#ObservedObject var fetcher = VideoFetcher()
...
init() {
// Causes reload each render
self.fetcher.load()
}
var body: some View {
...
.onAppear {
// Loads that on appear but fetcher.video is nil after view re-rendered because load() wasn't called
self.fetcher.load()
}
}
}
Thanks, Chris. I thought I was doing something wrong on an architectural level but I added caching and that solved my problem.
import Alamofire
import AlamofireImage
import Cache
public class VideoFetcher: ObservableObject {
#Published var video: VideoResponse?
#Published var coverImage: UIImage?
#Published var coverImageLoading = false
#Published var broadcasterImage: UIImage?
#Published var categories: String?
#Published var loading = false
#Published var error = false
func load(mediaItemSlug: String = "", broadcasterSlug: String = "") {
let videoCache = try? AppCache.video!.object(forKey: mediaItemSlug)
if (videoCache != nil) {
self.video = videoCache
self.setCategories()
self.loadCoverImage()
return
}
self.loading = true
Video.findBySlug(
mediaItemSlug: mediaItemSlug,
broadcasterSlug: broadcasterSlug,
successCallback: {video -> Void in
try? AppCache.video!.setObject(video, forKey: mediaItemSlug)
self.video = video
self.loading = false
self.setCategories()
self.loadCoverImage()
self.loadBroadcasterImage()
},
errorCallback: {(error, _) -> Void in
self.loading = false
self.error = true
})
}
func loadCoverImage() {
let coverImageUrl = self.video!.coverImageUrl
guard coverImageUrl != "" else {
return
}
let urlRequest = URLRequest(url: URL(string: coverImageUrl)!)
let cachedImage = AppCache.image!.image(for: urlRequest, withIdentifier: coverImageUrl)
if (cachedImage != nil) {
self.coverImage = cachedImage
return
}
self.coverImageLoading = true
let downloader = ImageDownloader(imageCache: AppCache.image!)
let filter = AspectScaledToFillSizeFilter(size: CGSize(width: 520.0, height: 292.499999963))
downloader.download(urlRequest, filter: filter) { response in
if case .success(let image) = response.result {
AppCache.image!.add(image, for: urlRequest, withIdentifier: coverImageUrl)
self.coverImage = image
self.coverImageLoading = false
}
}
}
func loadBroadcasterImage() {
let broadcasterImage = self.video!.broadcaster.avatarImageUrl
guard broadcasterImage != "" else {
return
}
let urlRequest = URLRequest(url: URL(string: broadcasterImage)!)
let cachedImage = AppCache.image!.image(for: urlRequest, withIdentifier: broadcasterImage)
if (cachedImage != nil) {
self.broadcasterImage = cachedImage
return
}
let downloader = ImageDownloader(imageCache: AppCache.image!)
let filter = AspectScaledToFillSizeFilter(size: CGSize(width: 16, height: 16))
downloader.download(urlRequest, filter: filter) { response in
if case .success(var image) = response.result {
image = image.af.imageRoundedIntoCircle()
AppCache.image!.add(image, for: urlRequest, withIdentifier: broadcasterImage)
self.broadcasterImage = image
}
}
}
func setCategories() {
let categories = self.video!.broadcaster.categories
if (categories.count > 0) {
let categoryNames = categories.map { category in
return category.name == "" ? "(no name)" : category.name
}
self.categories = categoryNames.joined(separator: " • ");
}
}
}