How can I change Mapbox style URL when device switches light/dark mode? - swift

In my SwiftUI app I am using Mapbox and have two style URL's: One for light-mode and one for dark-mode.
Right now I am using the code below to switch style URL's in my constants file which only works if I perform a fresh build of the app...
let MAPBOX_STYLE_URL : String = {
if UITraitCollection.current.userInterfaceStyle == .dark {
return "mapbox://styles/customDarkModeUrl"
}
else {
return "mapbox://styles/customLightModeUrl"
}
}()
Where can I make the app adapt the style URL every time the device switches to dark-mode?
Could I put this code in SceneDeletegate's willEnterForeGround or didBecomeActive?? I am not sure how to perform this style url update?
Thank you!

You just need to monitor your environment color scheme on your view:
struct ContentView: View {
#Environment(\.colorScheme) var colorScheme
var mapboxStyleURL: String {
"mapbox://styles/custom" + (colorScheme == .dark ? "Dark" : "Light") + "ModeUrl"
}
var body: some View {
Text(mapboxStyleURL)
}
}

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 - Stripe STPPaymentContext view not updating with STPPaymentConfiguration

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.

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()
}
}

How customise Slider blue line in SwiftUI?

Like in UISlider
let slider = UISlider()
slider.minimumTrackTintColor = .red
As pointed out in other answers you have limited ability to customize a Slider in SwiftUI. You can change the .accentColor(.red) but that only changes the minimumTrackTintColor.
Example of a Slider with .accentColor(.red)
Additionally, you can't change other things like thumbTintColor.
If you want more customization than just minimumTrackTintColor that you have no choice but to use a UISlider in SwiftUI as rob mayoff stated.
Here is some code on how you can use a UISlider in SwiftUI
struct SwiftUISlider: UIViewRepresentable {
final class Coordinator: NSObject {
// The class property value is a binding: It’s a reference to the SwiftUISlider
// value, which receives a reference to a #State variable value in ContentView.
var value: Binding<Double>
// Create the binding when you initialize the Coordinator
init(value: Binding<Double>) {
self.value = value
}
// Create a valueChanged(_:) action
#objc func valueChanged(_ sender: UISlider) {
self.value.wrappedValue = Double(sender.value)
}
}
var thumbColor: UIColor = .white
var minTrackColor: UIColor?
var maxTrackColor: UIColor?
#Binding var value: Double
func makeUIView(context: Context) -> UISlider {
let slider = UISlider(frame: .zero)
slider.thumbTintColor = thumbColor
slider.minimumTrackTintColor = minTrackColor
slider.maximumTrackTintColor = maxTrackColor
slider.value = Float(value)
slider.addTarget(
context.coordinator,
action: #selector(Coordinator.valueChanged(_:)),
for: .valueChanged
)
return slider
}
func updateUIView(_ uiView: UISlider, context: Context) {
// Coordinating data between UIView and SwiftUI view
uiView.value = Float(self.value)
}
func makeCoordinator() -> SwiftUISlider.Coordinator {
Coordinator(value: $value)
}
}
#if DEBUG
struct SwiftUISlider_Previews: PreviewProvider {
static var previews: some View {
SwiftUISlider(
thumbColor: .white,
minTrackColor: .blue,
maxTrackColor: .green,
value: .constant(0.5)
)
}
}
#endif
Then you can use this slider in your ContentView like this:
struct ContentView: View {
#State var sliderValue: Double = 0.5
var body: some View {
VStack {
Text("SliderValue: \(sliderValue)")
// Slider(value: $sliderValue).accentColor(.red).padding(.horizontal)
SwiftUISlider(
thumbColor: .green,
minTrackColor: .red,
maxTrackColor: .blue,
value: $sliderValue
).padding(.horizontal)
}
}
}
Example:
Link to full project
As of Apple's 2021 platforms, you can use the tint modifier to change the color of the track to the left of the slider knob. Beyond that, SwiftUI's Slider doesn't let you customize its appearance.
If you need more customization, then for now your only option is to create a UISlider and wrap it in a UIViewRepresentable. Work through the “Interfacing with UIKit” tutorial and watch WWDC 2019 Session 231: Integrating SwiftUI to learn how to use UIViewRepresentable.
The Slider documentation formerly mentioned a type named SliderStyle, but there is no documentation for SliderStyle and the type is not actually defined in the public interface of the SwiftUI framework as of Xcode 11 beta 4. It is possible that it will appear in a later release. It is also possible that we will have to wait for a future (after 13) version of SwiftUI for this ability.
If SliderStyle does appear, it might allow you to customize the appearance of a Slider in the same way that ButtonStyle lets you customize the appearance of Button—by assuming total responsibility for drawing it. So you might want to look for ButtonStyle tutorials on the net if you want to get a head start.
But SliderStyle might end up being more like TextFieldStyle. Apple provides a small number of TextFieldStyles for you to choose from, but you cannot define your own.
.accentColor(.red)
This will work on iOS and Mac Catalyst.
Check out customizable sliders example here
If the bright white slider handle grates on your dark mode design, you can use .label color and .softLight to tell it to simmer down. It looks good only in grayscale, unless you can figure out the blend modes and hue rotation.
The best looking result would be from an overlaid shaped with .blendMode(.sourceAtop)... but that blocks interaction, sadly.
#Environment(\.colorScheme) var colorScheme
var body: some View {
let hackySliderBGColor: Color = colorScheme == .dark ? Color(.secondarySystemBackground) : Color(.systemBackground)
let hackySliderAccentColor: Color = colorScheme == .dark ? Color(.label) : Color(.systemGray2)
let hackySliderBlendMode: BlendMode = colorScheme == .dark ? .softLight : .multiply
...
ZStack {
Rectangle()
.foregroundColor(hackySliderBGColor)
// This second Rect prevents a white sliver if slider is at max value.
.overlay(Rectangle()
.foregroundColor(hackySliderBGColor)
.offset(x: 5)
)
Slider(value: $pointsToScoreLimit,
in: themin...themax, step: 5)
.accentColor(hackySliderAccentColor)
.blendMode(hackySliderBlendMode)
}
Example:
So I tried to use .accentColor(.red) without success, so I noticed that for newer versions one has to use .tint(.red) to make the changes visible.
Hope this helps.
You can change the maximum track color using ZStack like this
var body: some View {
VStack {
Spacer()
Image("Fun").resizable().frame(width: 200, height: 200, alignment: .center).cornerRadius(20)
ZStack {
Rectangle()
.frame(height: 2)
.foregroundColor(.yellow).frame(width: UIScreen.main.bounds.width - 150)
Slider(value: $sliderval, in: 0...timeSlider_maximumValue, label: {Text("PLayer")}, minimumValueLabel: {Text("\(timeSlider_minimumValue)")}, maximumValueLabel: {Text("\(timeSlider_maximumValuetext)")}) { success in
SilderTap()
}.padding(.horizontal).tint(.green).foregroundColor(.white)
}
}
}
enter image description here