SwiftUI - Stripe STPPaymentContext view not updating with STPPaymentConfiguration - swift

So I have implemented Stripe into my project. Everything works as intended. However, I do wish to use some of the configuration options available to use. For instance, setting the default payment country to "UK". However, this causes my app to crash as it finds a nil value. I am also trying to use some of the other settings like so:
self.config.requiredBillingAddressFields = .full
self.config.appleMerchantIdentifier = "dummy-merchant-id"
self.config.cardScanningEnabled = true
self.config.applePayEnabled = true
The only thing within the view that I can seem to change is the .requiredBillingAddressFields. Everything else does not seem to register. My code is as followed, I've ripped out what is not related to Stripe for clarity:
struct CheckOutView: View {
#StateObject var paymentContextDelegate: PaymentContextDelegate
let config = STPPaymentConfiguration()
#State private var paymentContext: STPPaymentContext!
var body: some View {
HStack {
BorderedButton(text: "Pay Now") {
self.paymentContext.requestPayment()
}
Spacer()
}
HStack {
BorderedButton(text: paymentContextDelegate.paymentMethodButtonTitle) {
self.paymentContext.presentPaymentOptionsViewController()
}
Spacer()
}
.onAppear() {
DispatchQueue.main.async {
self.paymentContextConfiguration()
}
}
}
//MARK: - Configuration
func paymentContextConfiguration() {
self.config.requiredBillingAddressFields = .full
self.config.appleMerchantIdentifier = "dummy-merchant-id"
self.config.cardScanningEnabled = true
self.config.applePayEnabled = true
self.config.verifyPrefilledShippingAddress = true
self.config.canDeletePaymentOptions = true
self.paymentContext = STPPaymentContext(customerContext: customerContext, configuration: self.config, theme: .defaultTheme)
self.paymentContext.delegate = self.paymentContextDelegate
let keyWindow = UIApplication.shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?.windows
.filter({$0.isKeyWindow}).last
self.paymentContext.hostViewController = keyWindow?.rootViewController
}
}
Any help would be appreciated! Thank you.

This is more of a guess, but perhaps the config options are applying, but it's just that the other requirements for the features they enable are not met? How exactly do you determine that everything else except billing address does not register?
For example applePayEnabled will only result in the Apple Pay option appearing if the device itself supports Apple Pay(on a physical device with a real card in your wallet it would work(but you must have a real Apple Merchant ID), on simulators it can be patchy).
For cardScanningEnabled maybe you don't have the entitlement enabled in your app? https://github.com/stripe/stripe-ios#card-scanning-beta
A simple way to check if the config applies is maybe to set requiredShippingAddressFields since it's self-contained and very visual.

Related

SwiftUI alert and confirmation dialog preferredColorScheme

I've noticed when setting the preferredColorScheme in my SwiftUI app the .alert and .confirmationDialog modifiers don't use the colorScheme. Also the launch screen doesn't use the colorScheme either. It seems to be using system defaults, is this normal behaviour?
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.preferredColorScheme(.light)
}
}
}
I was also unable to get better results using UIKit's overrideUserInterfaceStyle. The launch screen is still using system defaults, unsure how to solve that in SwiftUI without a launch screen. Also the code becomes pretty ugly and much more extensive.
guard let scene = scenes.first as? UIWindowScene else { return }
scene.keyWindow?.overrideUserInterfaceStyle = .light // .light .dark .unspecified
I couldn't figure out a pure SwiftUI way, but I found a simple solution. Use UIKit, then handle the changes in SwiftUI. I just needed to use .overrideUserInterfaceStyle. As for the Launch Screen in SwiftUI, add a Background color key to the dictionary. Then add a Color setfrom your assets and set the Background color key value to the name of the asset created as a String.
let scenes = UIApplication.shared.connectedScenes
guard let scene = scenes.first as? UIWindowScene else { return nil }
scene.keyWindow?.overrideUserInterfaceStyle = .light //.dark, .light, .unspecified

SwiftUI - How to disable sidebar from collapsing?

Gif to understand easier
Is there any way to disable collapsibility of SidebarListStyle NavigationViews?
EDIT: This method still works as of late 2022, and has never stopped working on any version of macOS (up to latest Ventura 13.1). Not sure why there are answers here suggesting otherwise. If the Introspection library changes their API you may need to update your calls accordingly, but the gist of the solution is the same.
Using this SwiftUI Introspection library:
https://github.com/siteline/SwiftUI-Introspect
We can introspect the underlying NSSplitView by extending their functionality:
public func introspectSplitView(customize: #escaping (NSSplitView) -> ()) -> some View {
return introspect(selector: TargetViewSelector.ancestorOrSibling, customize: customize)
}
And then create a generic extension on View:
public extension View {
func preventSidebarCollapse() -> some View {
return introspectSplitView { splitView in
(splitView.delegate as? NSSplitViewController)?.splitViewItems.first?.canCollapse = false
}
}
}
Which can be used on our sidebar:
var body: some View {
(...)
MySidebar()
.preventSidebarCollapse()
}
The introspection library mentioned by Oskar is not working for MacOS.
Inspired by that, I figured out a solution for MacOS.
The rationality behind the solution is to use a subtle way to find out the parent view of a NavigationView which is a NSSplitViewController in the current window.
Below codes was tested on XCode 13.2 and macOS 12.1.
var body: some View {
Text("Replace with your sidebar view")
.onAppear {
guard let nsSplitView = findNSSplitVIew(view: NSApp.windows.first?.contentView), let controller = nsSplitView.delegate as? NSSplitViewController else {
return
}
controller.splitViewItems.first?.canCollapse = false
// set the width of your side bar here.
controller.splitViewItems.first?.minimumThickness = 150
controller.splitViewItems.first?.maximumThickness = 150
}
}
private func findNSSplitVIew(view: NSView?) -> NSSplitView? {
var queue = [NSView]()
if let root = view {
queue.append(root)
}
while !queue.isEmpty {
let current = queue.removeFirst()
if current is NSSplitView {
return current as? NSSplitView
}
for subview in current.subviews {
queue.append(subview)
}
}
return nil
}
While the method that Oskar used with the Introspect library no longer works, I did find another way of preventing the sidebar from collapsing using Introspect. First, you need to make an extension on View:
extension View {
public func introspectSplitView(customize: #escaping (NSSplitView) -> ()) -> some View {
return inject(AppKitIntrospectionView(
selector: { introspectionView in
guard let viewHost = Introspect.findViewHost(from: introspectionView) else {
return nil
}
return Introspect.findAncestorOrAncestorChild(ofType: NSSplitView.self, from: viewHost)
},
customize: customize
))
}
}
Then do the following:
NavigationView {
SidebarView()
.introspectSplitView { controller in
(controller.delegate as? NSSplitViewController)?.splitViewItems.first?.canCollapse = false
}
Text("Main View")
}
This being said, we don't know how long this will actually work for. Apple could change how NavigationView works and this method may stop working in the future.

SwiftUI - Audio keeps playing when changing views

I have a problem that's cropped up since I've updated my Xcode... Unless the user hits pause in the audio view. My audio player continues to play when the user changes views however I would like the audio to stop when the user exits the player view (ItemDetail) (for example when the user goes to Content View)
Previously I was using the below at the start of the Content View and that had worked but now it doesn't:
init() { sounds.pauseSounds() }
I've also tried this (which hasn't worked either):
}// end of navigation view .onAppear{sounds.pauseSounds()}
This is my sounds class:
class Sounds:ObservableObject {
var player:AVAudioPlayer?
// let shared = Sounds()
func playSounds(soundfile: String) {
if let path = Bundle.main.path(forResource: soundfile, ofType: nil){
do{
player = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
player?.prepareToPlay()
player?.play()
}catch {
print("Error can't find sound file or something's not working with the sounds model")
}
}
}
// This method used to work at pausing the sound (pre-update)
func pauseSounds() {
player?.pause()
}
// over rides the sound defaulting to bluetooth speaker only and sends it to phone speaker if bluetooth is not available (headset/speaker) however if it is available it'll play through that.
func overRideAudio() {
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(AVAudioSession.Category.playback, mode: .spokenAudio, options: .defaultToSpeaker)
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
} catch {
print("error.")
}
}
}
And this is the player view:
struct ItemDetail: View {
#State var isPlaying = false
var item : SessionsItem
#ObservedObject var soundsPlayer = Sounds()
var body: some View {
HStack {
Button(action: {
if self.isPlaying{
// Sounds.player?.pause()
self.isPlaying = false
soundsPlayer.pauseSounds()
}
else{
self.isPlaying = true
soundsPlayer.playSounds(soundfile: "\(self.item.name).mp3")
soundsPlayer.overRideAudio()
}
You are creating another Sound instance so you do not have access to the open audiio instance. You can make Sounds class as single instance or pass the Sounds instance to the detail. Dont create a new one.
onAppear is not a reliable way to do work like this SwiftUI does a lot of preloading (It is very apparent with a List). It depends on the device and its capabilities. Sometimes screens "appear" when they are preloaded not necessarily when the user is looking at it.
With the List example below you can see it in simulator.
On iPhone 8 I get 12 onAppear print statements in the console when I initially load it and on iPhone 12 Pro Max I get 17 print statements.
If the cells are more/less complex you get more/less statements accordingly. You should find another way to do this.
import SwiftUI
struct ListCellAppear: View {
var idx: Int
var body: some View {
HStack{
Text(idx.description)
Image(systemName: "checkmark")
}.onAppear(){
print(idx.description)
}
}
}
struct ListAppear: View {
var body: some View {
List(0..<1000){ idx in
ListCellAppear(idx: idx).frame(height: 300)
}
}
}
struct ListAppear_Previews: PreviewProvider {
static var previews: some View {
ListAppear()
}
}

Sharing Screenshot of SwiftUI view causes crash

I am grabbing a screenshot of a sub-view in my SwiftUI View to immediately pass to a share sheet in order to share the image.
The view is of a set of questions from a text array rendered as a stack of cards. I am trying to get a screenshot of the question and make it share-able along with a link to the app (testing with a link to angry birds).
I have been able to capture the screenshot using basically Asperi's answer to the below question:
How do I render a SwiftUI View that is not at the root hierarchy as a UIImage?
My share sheet launches, and I've been able to use the "Copy" feature to copy the image, so I know it's actually getting a screenshot, but whenever I click "Message" to send it to someone, or if I just leave the share sheet open, the app crashes.
The message says it's a memory issue, but doesn't give much description of the problem. Is there a good way to troubleshoot this sort of thing? I assume it must be something with how the screenshot is being saved in this case.
Here are my extensions of View and UIView to render the image:
extension UIView {
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}
}
extension View {
func asImage() -> UIImage {
let controller = UIHostingController(rootView: self)
// locate far out of screen
controller.view.frame = CGRect(x: 0, y: CGFloat(Int.max), width: 1, height: 1)
UIApplication.shared.windows.first!.rootViewController?.view.addSubview(controller.view)
let size = controller.sizeThatFits(in: UIScreen.main.bounds.size)
controller.view.bounds = CGRect(origin: .zero, size: size)
controller.view.sizeToFit()
controller.view.backgroundColor = .clear
let image = controller.view.asImage()
controller.view.removeFromSuperview()
return image
}
}
Here's an abbreviated version of my view - the button is about halfway down, and should call the private function at the bottom that renders the image from the View/UIView extensions, and sets the "questionScreenShot" variable to the rendered image, which is then presented in the share sheet.
struct TopicPage: View {
var currentTopic: Topic
#State private var currentQuestions: [String]
#State private var showShareSheet = false
#State var questionScreenShot: UIImage? = nil
var body: some View {
GeometryReader { geometry in
Button(action: {
self.questionScreenShot = render()
if self.questionScreenShot != nil {
self.showShareSheet = true
} else {
print("Did not set screenshot")
}
}) {
Text("Share Question").bold()
}
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [questionScreenShot!])
}
}
}
private func render() -> UIImage {
QuestionBox(currentQuestion: self.currentQuestions[0]).asImage()
}
}
I've found a solution that seems to be working here. I start the variable where the questionScreenShot gets stored as nil to start:
#State var questionScreenShot: UIImage? = nil
Then I just make sure to set it to 'render' when the view appears, which means it loads the UIImage so if the user clicks "Share Question" it will be ready to be loaded (I think there was an issue earlier where the UIImage wasn't getting loaded in time once the sharing was done).
It also sets that variable back to nil on disappear.
.onAppear {
self.currentQuestions = currentTopic.questions.shuffled()
self.featuredQuestion = currentQuestions.last!
self.questionScreenShot = render()
}
.onDisappear {
self.questionScreenShot = nil
self.featuredQuestion = nil
}

Agora SwiftUI OpenLive Integration issues

I'm trying to implement Agora to my existing SwiftUI app, I used this tutorial to implement it https://github.com/AgoraIO/Basic-Video-Call/tree/master/One-to-One-Video/Agora-iOS-Tutorial-SwiftUI-1to1
My Intention is to have a OpenLive video calls where I have only one Broadcaster and many audience to view it (similar to Instagram Live). unfortunately, I only found documentation on how to make a 1-1 video call and I was not able to let Audience join the channel and preview the live video stream.
This is my code:
import SwiftUI
import AgoraRtcKit
var isCurrentStreamer = true
struct AgoraView : View {
#EnvironmentObject var userDefaultData: UserDefaultDetails
#State var isLocalInSession = true
#State var isLocalAudioMuted = false
#State var isRemoteInSession = true
#State var isRemoteVideoMuted = false
let localCanvas = VideoCanvas()
let remoteCanvas = VideoCanvas()
private let videoEngine = VideoEngine()
private var rtcEngine: AgoraRtcEngineKit {
get {
return videoEngine.agoraEngine
}
}
var body: some View {
ZStack() {
VideoSessionView(
backColor: Color("c2"),
backImage: Image("big_logo"),
hideCanvas: false,
canvas: isCurrentStreamer ? localCanvas:remoteCanvas
).edgesIgnoringSafeArea(.all)
}.onAppear {
self.agoraLive(role: self.userDefaultData.AgoraRole, channelID: self.userDefaultData.currentAuctionId)
}
}
}
fileprivate extension AgoraView {
func agoraLive(role: AgoraClientRole, channelID: String){
// init AgoraRtcEngineKit
videoEngine.AgoraView = self
rtcEngine.enableDualStreamMode(false)
rtcEngine.setVideoEncoderConfiguration(
AgoraVideoEncoderConfiguration(
size: AgoraVideoDimension640x360,
frameRate: .fps15,
bitrate: AgoraVideoBitrateStandard,
orientationMode: .adaptative
)
)
if role == .broadcaster {
rtcEngine.enableVideo()
addLocalSession()
rtcEngine.startPreview()
}else{
addLocalSession()
rtcEngine.startPreview()
}
rtcEngine.joinChannel(byToken: Token, channelId: channelID, info: nil, uid: 0, joinSuccess: nil)
// Step 6, set speaker audio route
rtcEngine.setEnableSpeakerphone(true)
}
func addLocalSession() {
let videoCanvas = AgoraRtcVideoCanvas()
if self.userDefaultData.AgoraRole == .broadcaster {
videoCanvas.view = localCanvas.rendererView
}else {
videoCanvas.view = remoteCanvas.rendererView
}
videoCanvas.renderMode = .hidden
if isCurrentStreamer {
rtcEngine.setupLocalVideo(videoCanvas)
}else{
rtcEngine.setupRemoteVideo(videoCanvas)
}
}
}
Thanks.
It looks like you aren't hooking up the remote video view correctly. In order to see a remote user's video, you need to create a AgoraRtcVideoCanvas with that remote user's UID, and then pass it into setupRemoteVideo. What you're doing is trying to set up a remote view before you've even joined the call, and have no idea who is in it.
You need to implement the remoteVideoStateChangedOfUid callback on the AgoraRtcEngineDelegate in order to get the remote user's UID and hook it up to your canvas correctly.
Something like this:
extension AgoraView: AgoraRtcEngineDelegate {
func rtcEngine(_ engine: AgoraRtcEngineKit, remoteVideoStateChangedOfUid uid: UInt, state: AgoraVideoRemoteState, reason: AgoraVideoRemoteStateReason, elapsed: Int) {
if state == .starting {
let videoCanvas = AgoraRtcVideoCanvas()
videoCanvas.view = remoteCanvas.rendererView
videoCanvas.uid = uid
videoCanvas.renderMode = .hidden
rtcEngine.setupRemoteVideo(videoCanvas)
}
}
}
If you did the above as written, you'd have to set the AgoraView as the rtcEngine's delegate when you initialize it, as well.
If you're using the demo app provided, VideoEngine is currently set up as the delegate, and is doing this in firstRemoteVideoDecodedOfUid (which is now deprecated). It's likely that this code is not correctly hooked up to your SwiftUI views.