I am trying to implement notifications in my SwiftUI app, but have not managed to do so after extensive research on Google and other search engines.
Here's a simple example on how to use notifications in a SwiftUI app. It requests for permissions and checks if they are granted, and allows you to send a notification if so.
Do note that notifications from apps won't appear if the app they originate from is in the foreground, so I demonstrate closing the app quickly before the notification is sent to view it.
Here's the whole example code the demo uses:
import SwiftUI
import UserNotifications
struct ContentView: View {
#State private var permissionGranted = false
private func requestPermissions() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
if success {
permissionGranted = true
} else if let error = error {
print(error.localizedDescription)
}
}
}
private func sendNotification() {
let notificationContent = UNMutableNotificationContent()
notificationContent.title = "Hello world!"
notificationContent.subtitle = "Here's how you send a notification in SwiftUI"
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
// you could also use...
// UNCalendarNotificationTrigger(dateMatching: .init(year: 2022, month: 12, day: 10, hour: 0, minute: 0), repeats: true)
let req = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: trigger)
UNUserNotificationCenter.current().add(req)
}
var body: some View {
VStack {
if !permissionGranted {
Button("Request Permission") {
requestPermissions()
}
}
if permissionGranted {
Button("Send Notification") {
sendNotification()
}
}
}
.onAppear {
// Check if we already have permissions to send notifications
UNUserNotificationCenter.current().getNotificationSettings { settings in
if settings.authorizationStatus == .authorized {
permissionGranted = true
}
}
}
.padding()
}
}
And here's a demo video of the following code:
Related
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 have created a class to perform a network request and parse the data using Combine. I'm not entirely certain the code is correct, but it's working as of now (still learning the basics of Swift and basic networking tasks). My Widget has the correct data and is works until the data becomes nil. Unsure how to check if the data from my first publisher in my SwiftUI View is nil, the data seems to be valid even when there's no games showing.
My SwiftUI View
struct SimpleEntry: TimelineEntry {
let date: Date
public var model: CombineData?
let configuration: ConfigurationIntent
}
struct Some_WidgetEntryView : View {
var entry: Provider.Entry
#Environment(\.widgetFamily) var widgetFamily
var body: some View {
VStack (spacing: 0){
if entry.model?.schedule?.dates.first?.games == nil {
Text("No games Scheduled")
} else {
Text("Game is scheduled")
}
}
}
}
Combine
import Foundation
import WidgetKit
import Combine
// MARK: - Combine Attempt
class CombineData {
var schedule: Schedule?
var live: Live?
private var cancellables = Set<AnyCancellable>()
func fetchSchedule(_ teamID: Int, _ completion: #escaping (Live) -> Void) {
let url = URL(string: "https://statsapi.web.nhl.com/api/v1/schedule?teamId=\(teamID)")!
let publisher = URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: Schedule.self, decoder: JSONDecoder())
//.catch { _ in Empty<Schedule, Error>() }
//.replaceError(with: Schedule(dates: []))
let publisher2 = publisher
.flatMap {
return self.fetchLiveFeed($0.dates.first?.games.first?.link ?? "")
}
Publishers.Zip(publisher, publisher2)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: {_ in
}, receiveValue: { schedule, live in
self.schedule = schedule
self.live = live
completion(self.live!)
WidgetCenter.shared.reloadTimelines(ofKind: "NHL_Widget")
}).store(in: &cancellables)
}
func fetchLiveFeed(_ link: String) -> AnyPublisher<Live, Error /*Never if .catch error */> {
let url = URL(string: "https://statsapi.web.nhl.com\(link)")!
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: Live.self, decoder: JSONDecoder())
//.catch { _ in Empty<Live, Never>() }
.eraseToAnyPublisher()
}
}
Like I said in the comments, it's likely that the decode(type: Live.self, decoder: JSONDecoder()) returns an error because the URL that you're fetching from when link is nil doesn't return anything that can be decoded as Live.self.
So you need to handle that case somehow. For example, you can handle this by making the Live variable an optional, and returning nil when link is empty (or nil).
This is just to set you in the right direction - you'll need to work out the exact code yourself.
let publisher2 = publisher1
.flatMap {
self.fetchLiveFeed($0.dates.first?.games.first?.link ?? "")
.map { $0 as Live? } // convert to an optional
.replaceError(with: nil)
}
Then in the sink, handle the nil:
.sink(receiveCompletion: {_ in }, receiveValue:
{ schedule, live in
if let live = live {
// normal treatment
self.schedule = schedule
self.live = live
//.. etc
} else {
// set a placeholder
}
})
SwiftUI and WidgetKit work differently. I needed to fetch data in getTimeline for my IntentTimelineProvider then add a completion handler for my TimelineEntry. Heavily modified my Combine data model. All credit goes to #EmilioPelaez for pointing me in the right direction, answer here.
I would like to add leaderboards to my SwiftUI app.
I can't find any examples of using loadEntries to load leaderboard values.
I tried the following...
let leaderBoard: GKLeaderboard = GKLeaderboard()
leaderBoard.identifier = "YOUR_LEADERBOARD_ID_HERE"
leaderBoard.timeScope = .allTime
leaderBoard.loadScores { (scores, error) in ...
This results in the following warnings:
'identifier' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler: instead.
'timeScope' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler: instead.
'loadScores(completionHandler:)' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler:.
using loadEntriesForPlayerScope results in the following warning:
'loadEntriesForPlayerScope(_:timeScope:range:completionHandler:)' has
been renamed to 'loadEntries(for:timeScope:range:completionHandler:)'
Using loadEntries I don't know how to specify the leaderboard identifier.
Here is simple demo of possible approach - put everything in view model and load scores on view appear.
import GameKit
class BoardModel: ObservableObject {
private var board: GKLeaderboard?
#Published var localPlayerScore: GKLeaderboard.Entry?
#Published var topScores: [GKLeaderboard.Entry]?
func load() {
if nil == board {
GKLeaderboard.loadLeaderboards(IDs: ["YOUR_LEADERBOARD_ID_HERE"]) { [weak self] (boards, error) in
self?.board = boards?.first
self?.updateScores()
}
} else {
self.updateScores()
}
}
func updateScores() {
board?.loadEntries(for: .global, timeScope: .allTime, range: NSRange(location: 1, length: 10),
completionHandler: { [weak self] (local, entries, count, error) in
DispatchQueue.main.async {
self?.localPlayerScore = local
self?.topScores = entries
}
})
}
}
struct DemoGameboardview: View {
#StateObject var vm = BoardModel()
var body: some View {
List {
ForEach(vm.topScores ?? [], id: \.self) { item in
HStack {
Text(item.player.displayName)
Spacer()
Text(item.formattedScore)
}
}
}
.onAppear {
vm.load()
}
}
}
I might be stating the obvious but have you looked at the WWDC20 videos?
Usually when there are big changes like this they cover it during WWDC that year.
Tap into Game Center: Leaderboards, Achievements, and Multiplayer
Tap into Game Center: Dashboard, Access Point, and Profile
I haven't looked at the videos but the documentation eludes that identifier might be replaced by var baseLeaderboardID: String
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'm trying to dismiss a .sheet in SwiftUI, after calling an async process to confirm the user's MFA code. (I'm using the AWS Amplify Framework).
I have a binding variable set on the main view, and reference it in the view the sheet presents with #Binding var displayMFAView: Bool. I have an authentication helper that tracks the user state: #EnvironmentObject var userAuthHelper: UserAuthHelper.
The following code dismisses the sheet as expected:
func confirmMFACode(verificationCode: String) {
// Code to confifm MFA...
print("User confirmed MFA")
self.userAuthHelper.isSignedIn = true
self.displayMFAView = false
}
However, if I call the auth process via Amplify's confirmSignIn method,
func confirmVerificationMFA(verificationCode: String) {
AWSMobileClient.default().confirmSignIn(challengeResponse: verificationCode) { (signInResult, error) in
if let error = error as? AWSMobileClientError {
// ... error handling ...
} else if let signInResult = signInResult {
switch (signInResult.signInState) {
case .signedIn:
print("User confirmed MFA")
self.userAuthHelper.isSignedIn = true
self.displayMFAView = false
default:
print("\(signInResult.signInState.rawValue)")
}
}
}
}
the sheet does not get dismissed. I have tried wrapping the variable assignment in DispatchQueue.main.async {..., but that hasn't solved the issue either.
...
DispatchQueue.main.async {
self.userAuthHelper.isSignedIn = true
self.displayMFAView = false
}
...
In fact, this throws the following into my logs:
Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
Wrapping the switch (... in a DispatchQueue per https://stackoverflow.com/a/58288437/217101 gave me the same warning in my log.
Admittedly I don't have a firm grasp on SwiftUI or AWS Amplify. What am I not understanding?
From what I can tell the async call does something unexpected with the state variables, but not with an EnvironmentObject. So, nstead of #Binding var displayMFAView: Bool, I stored displayMFAView in an EnvironmentObject,
#EnvironmentObject var settings: UserSettings
#State var mfaCode: String = ""
and then can show or hide the .sheet(... by updating a boolean in that object:
Button(action: {
self.signIn() // Async call happens here
self.settings.displayMFAView.toggle()
}) {
Text("Sign In")
}.sheet(isPresented: self.$settings.displayMFAView) {
// Example code to capture text
TextField("Enter your MFA code", text: self.$mfaCode)
}
Button(action: {
self.verifyMFACode(verificationCode: self.mfaCode) // async call
}) {
Text("Confirm")
}
In func verifyMFACode(), I can make an async call to validate my user, then toggle the sheet to disappear on success:
func verifyMFACode(verificationCode: String) {
AWSMobileClient.default().confirmSignIn(challengeResponse: verificationCode) {
...
case .signedIn:
self.settings.displayMFAView.toggle()
...