Weird behavior when using SwiftUI View as accessory view for NSSavePanel - swift

I'm trying to use a view written in SwiftUI as an accessory view of my NSSavePanel but I struggled to get it working properly.
Here's the implementation for my SwiftUI view:
struct ExportAccessoryView: View {
enum ExportFileType: String, Identifiable {
// ... enum declaration
}
#State var selectedExportFileType: ExportFileType = .png
#State var resolution = 256.0
#Binding var selectedFileTypeBinding: ExportFileType
#Binding var resolutionBinding: Double
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Picker(selection: $selectedExportFileType, label: Text("Format:")) {
Text("PDF").tag(ExportFileType.pdf)
// ... other items
}
.frame(width: 170)
.padding(.leading, 21)
if [ExportFileType.png, ExportFileType.jpeg, ExportFileType.tiff].contains(selectedExportFileType) {
HStack {
Slider(value: $resolution, in: 128...1024,
label: { Text("Resolution:") })
.frame(width: 200)
Text("\(Int(resolution))")
.frame(width: 40, alignment: .leading)
.padding(.leading, 5)
}
}
}
.padding(10)
.onChange(of: selectedExportFileType) { newValue in
self.selectedFileTypeBinding = newValue
}
.onChange(of: resolution) { newValue in
self.resolutionBinding = newValue
}
}
}
Here's how I implemented my save panel:
class DocumentWindow: NSWindowController {
var exportFileType: ExportAccessoryView.ExportFileType = .pdf
var resolution = 256.0
lazy var exportPanel: NSSavePanel = {
let savePanel = NSSavePanel()
savePanel.message = "Specify where and how you wish to export..."
savePanel.nameFieldLabel = "Export As:"
savePanel.canCreateDirectories = true
savePanel.isExtensionHidden = false
savePanel.showsTagField = true
let fileTypeBinding = Binding {
return self.exportFileType
} set: { newValue in
self.exportFileType = newValue
// update file extension
self.exportPanel.allowedContentTypes = [UTType(newValue.rawValue)!]
}
let resolutionBinding = Binding {
return self.resolution
} set: { newValue in
self.resolution = newValue
}
let accessoryView = ExportAccessoryView(selectedFileTypeBinding: fileTypeBinding,
resolutionBinding: resolutionBinding)
let exportAccessoryView = NSHostingController(rootView: accessoryView)
savePanel.accessoryView = exportAccessoryView.view
savePanel.allowedContentTypes = [UTType(self.exportFileType.rawValue)!]
return savePanel
}()
}
The save panel is presented by invoking beginSheetModal(for:completionHandler:).
It has no problem displaying but the accessory view is exhibiting some bizarre behavior: it seems to be doing its own thing at random (I sought for patterns but I failed to do so).
Sometimes it works properly, sometimes it becomes unclickable (but the function is still accessible via switch control using TAB). The alignment is always different from the last time I expanded/collapsed or opened/closed the panel: sometimes it's left aligned, sometimes it's centered (even if I have explicitly opted for .leading for alignment).
I have absolutely no idea what's going on. I don't know if this is an issue with SwiftUI+AppKit or is it that I'm doing it all wrong, which is highly likely since I'm a total newbie in SwiftUI. What should I do to get it working properly?

I remembered from back in the days when I was using XIB for implementing an accessory view: I used to embed the controls within an NSView and then set up constraints to make it work. So I applied the same idea here of embedding the NSHostingView's view within a custom NSView and after tweaking it for a bit, I made it work:
lazy var exportPanel: NSSavePanel = {
// ... setting up save panel
// instantiate SwiftUI view and its hosting controller
let accessoryView = ExportAccessoryView(selectedFileTypeBinding: fileTypeBinding,
resolutionBinding: resolutionBinding)
let exportAccessoryView = NSHostingController(rootView: accessoryView)
// embed the SwiftUI in a custom view
let customView = NSView(frame: NSRect(x: 0, y: 0, width: 300, height: 60))
customView.addSubview(exportAccessoryView.view)
// use my own constraints
exportAccessoryView.view.translatesAutoresizingMaskIntoConstraints = false
// top and bottom clipped to custom view
exportAccessoryView.view.topAnchor.constraint(equalTo: customView.topAnchor).isActive = true
exportAccessoryView.view.bottomAnchor.constraint(equalTo: customView.bottomAnchor).isActive = true
// leading and trailing spaces can stretch as far as they need to be, hence ≥0
exportAccessoryView.view.leadingAnchor.constraint(greaterThanOrEqualTo: customView.leadingAnchor).isActive = true
exportAccessoryView.view.trailingAnchor.constraint(greaterThanOrEqualTo: customView.trailingAnchor).isActive = true
// center the SwiftUI view horizontal within custom view
exportAccessoryView.view.centerXAnchor.constraint(equalTo: customView.centerXAnchor).isActive = true
// usually fixed width and height
// can be flexible when SwiftUI view is dynamic
exportAccessoryView.view.widthAnchor.constraint(equalToConstant: customView.frame.width).isActive = true
exportAccessoryView.view.heightAnchor.constraint(greaterThanOrEqualToConstant: customView.frame.height).isActive = true
savePanel.accessoryView = customView
// ... additional setup
return savePanel
}()
Now it works perfectly as expected. Don't know if this is the "proper way" to implement such integration.

Related

SwiftUI center align a view except if another view "pushes" it up

Here is a breakdown.
I have a zstack that contains 2 vstacks.
first vstack has a spacer and an image
second has a text and button.
ZStack {
VStack {
Spacer()
Image("some image")
}
VStack {
Text("press the button")
Button("ok") {
print("you pressed the button")
}
}
}
Now this setup would easily give me an image on the bottom of a zstack, and a centered title and button.
However if for example the device had a small screen or an ipad rotates to landscape. depending on the image size (which is dynamic). The title and button will overlap the image. instead of the button being "pushed" up.
In UIKit this is as simple as centering the button to superview with a high priority and having greaterThanOrEqualTo image.topAnchor with a required priority.
button would be centered in screen but if the top of the image was too big the center constraint would give priority to the image top anchor required constraint and push the button up.
I have looked into custom alignments and can easily get always above image or always center but am missing some insight in having it both depending on layout. Image size is dynamic so no hardcoded sizes.
What am i missing here? how would you solve this simple yet tricky task.
There might be an easier way using .alignmentGuide but I tried to practice on Layout for this answer.
I created a custom ImageAndButtonLayout that should do what you want: it takes two views assuming the first is the image and the second is the button (or anything else).
They are put into subviews just for clarity, you can also put them directly into ImageAndButtonLayout. For testing you can change the height of the image via slider.
The Layout always uses the available full height and pushes the first view (image) to the bottom - so you don't need an extra Spacer() with the image. The position of the second view (button) is calculated based on the height of the first view and the available height.
struct ContentView: View {
#State private var imageHeight = 200.0 // for testing
var body: some View {
VStack {
ImageAndButtonLayout {
imageView
buttonView
}
// changing "image" height for testing
Slider(value: $imageHeight, in: 50...1000)
.padding()
}
}
var imageView: some View {
Color.teal // Image placeholder
.frame(height: imageHeight)
}
var buttonView: some View {
VStack {
Text("press the button")
Button("ok") {
print("you pressed the button")
}
}
}
}
struct ImageAndButtonLayout: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let maxsizes = subviews.map { $0.sizeThatFits(.infinity) }
var totalWidth = maxsizes.max {$0.width < $1.width}?.width ?? 0
totalWidth = min(totalWidth, proposal.width ?? .infinity )
let totalHeight = proposal.height ?? .infinity // always return maximum height
return CGSize(width: totalWidth, height: totalHeight)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let heightImage = subviews.first?.sizeThatFits(.unspecified).height ?? 0
let heightButton = subviews.last?.sizeThatFits(.unspecified).height ?? 0
let maxHeightContent = bounds.height
// place image at bottom, growing upwards
let ptBottom = CGPoint(x: bounds.midX, y: bounds.maxY) // bottom of screen
if let first = subviews.first {
var totalWidth = first.sizeThatFits(.infinity).width
totalWidth = min(totalWidth, proposal.width ?? .infinity )
first.place(at: ptBottom, anchor: .bottom, proposal: .init(width: totalWidth, height: maxHeightContent))
}
// place button at center – or above image
var centerY = bounds.midY
if heightImage > maxHeightContent / 2 - heightButton {
centerY = maxHeightContent - heightImage
centerY = max ( heightButton * 2 , centerY ) // stop at top of screen
}
let ptCenter = CGPoint(x: bounds.midX, y: centerY)
if let last = subviews.last {
last.place(at: ptCenter, anchor: .center, proposal: .unspecified)
}
}
}

How set Position of window on the Desktop in SwiftUI?

How to set window coordinates in SwiftUI on MacOS Desktop? For example, should the window appear always in the center or always in the upper right corner?
Here is my version, however, I shift the code and close it, when I open it, it appears first in the old place, and then jumps to a new place.
import SwiftUI
let WIDTH: CGFloat = 400
let HEIGTH: CGFloat = 200
#main
struct ForVSCode_MacOSApp: App {
#State var window : NSWindow?
var body: some Scene {
WindowGroup {
ContentView(win: $window)
}
}
}
struct WindowAccessor: NSViewRepresentable{
#Binding var window: NSWindow?
func makeNSView(context: Context) -> some NSView {
let view = NSView()
let width = (NSScreen.main?.frame.width)!
let heigth = (NSScreen.main?.frame.height)!
let resWidth: CGFloat = (width / 2) - (WIDTH / 2)
let resHeigt: CGFloat = (heigth / 2) - (HEIGTH / 2)
DispatchQueue.main.async {
self.window = view.window
self.window?.setFrameOrigin(NSPoint(x: resWidth, y: resHeigt))
self.window?.setFrameAutosaveName("mainWindow")
self.window?.isReleasedWhenClosed = false
self.window?.makeKeyAndOrderFront(nil)
}
return view
}
func updateNSView(_ nsView: NSViewType, context: Context) {
}
}
and ContentView
import SwiftUI
struct ContentView: View {
#Binding var win: NSWindow?
var body: some View {
VStack {
Text("it finally works!")
}
.font(.largeTitle)
.frame(width: WIDTH, height: HEIGTH, alignment: .center)
.background(WindowAccessor(window: $win))
}
}
struct ContentView_Previews: PreviewProvider {
#Binding var win: NSWindow?
static var previews: some View {
ContentView(win: .constant(NSWindow()))
.frame(width: 250, height: 150, alignment: .center)
}
}
I do have the same issue in one of my projects and thought I will investigate a bit deeper and I found two approaches to control the window position.
So my first approach to influence the window position is by pre-defining the windows last position on screen.
Indirect control: Frame autosave name
When the first window of an app is opened, macOS will try to restore the last window position when it was last closed. To distinguish the different windows, each window has its own frameAutosaveName.
The windows frame is persisted automatically in a text format in the apps preferences (UserDefaults.standard) with the key derived from the frameAutosaveName: "NSWindow Frame <frameAutosaveName>" (see docs for saveFrame).
If you do not specify an ID in your WindowGroup, SwiftUI will derive the autosave name from your main views class name. The first three windows will have the following autosave names:
<ModuleName>.ContentView-1-AppWindow-1
<ModuleName>.ContentView-1-AppWindow-2
<ModuleName>.ContentView-1-AppWindow-3
By setting an ID for example WindowGroup(id: "main"), the following autosave names are used (again for the first three windows):
main-AppWindow-1
main-AppWindow-2
main-AppWindow-3
When you check in your apps preferences directory (where UserDefaults.standard is stored), you will see in the plist one entry:
NSWindow Frame main-AppWindow-1 1304 545 400 228 0 0 3008 1228
There are a lot of numbers to digest. The first 4 integers describe the windows frame (origin and size), the next 4 integers describe the screens frame.
There are a few things to keep in mind when manually setting those value:
macOS coordinate system has it origin (0,0) in the bottom left corner.
the windows height includes the window title bar (28px on macOS Monterey but may be different on other versions)
the screens height excludes the title bar
I don't have documentation on this format and used trial and error to gain knowledge about it...
So to fake the initial position in the center of the screen I used the following function which I run in the apps (or the ContentView) initializer. But keep in mind: with this method only the first window will be centered. All the following windows are going to be put down and right of the previous window.
func fakeWindowPositionPreferences() {
let main = NSScreen.main!
let screenWidth = main.frame.width
let screenHeightWithoutMenuBar = main.frame.height - 25 // menu bar
let visibleFrame = main.visibleFrame
let contentWidth = WIDTH
let contentHeight = HEIGHT + 28 // window title bar
let windowX = visibleFrame.midX - contentWidth/2
let windowY = visibleFrame.midY - contentHeight/2
let newFramePreference = "\(Int(windowX)) \(Int(windowY)) \(Int(contentWidth)) \(Int(contentHeight)) 0 0 \(Int(screenWidth)) \(Int(screenHeightWithoutMenuBar))"
UserDefaults.standard.set(newFramePreference, forKey: "NSWindow Frame main-AppWindow-1")
}
My second approach is by directly manipulating the underlying NSWindow similar to your WindowAccessor.
Direct control: Manipulating NSWindow
Your implementation of WindowAccessor has a specific flaw: Your block which is reading view.window to extract the NSWindow instance is run asynchronously: some time in the future (due to DispatchQueue.main.async).
This is why the window appears on screen on the SwiftUI configured position, then disappears again to finally move to your desired location. You need more control, which involves first monitoring the NSView to get informed as soon as possible when the window property is set and then monitoring the NSWindow instance to get to know when the view is becoming visible.
I'm using the following implementation of WindowAccessor. It takes a onChange callback closure which is called whenever window is changing. First it starts monitoring the NSViews window property to get informed when the view is added to a window. When this happened, it starts listening for NSWindow.willCloseNotification notifications to detect when the window is closing. At this point it will stop any monitoring to avoid leaking memory.
import SwiftUI
import Combine
struct WindowAccessor: NSViewRepresentable {
let onChange: (NSWindow?) -> Void
func makeNSView(context: Context) -> NSView {
let view = NSView()
context.coordinator.monitorView(view)
return view
}
func updateNSView(_ view: NSView, context: Context) {
}
func makeCoordinator() -> WindowMonitor {
WindowMonitor(onChange)
}
class WindowMonitor: NSObject {
private var cancellables = Set<AnyCancellable>()
private var onChange: (NSWindow?) -> Void
init(_ onChange: #escaping (NSWindow?) -> Void) {
self.onChange = onChange
}
/// This function uses KVO to observe the `window` property of `view` and calls `onChange()`
func monitorView(_ view: NSView) {
view.publisher(for: \.window)
.removeDuplicates()
.dropFirst()
.sink { [weak self] newWindow in
guard let self = self else { return }
self.onChange(newWindow)
if let newWindow = newWindow {
self.monitorClosing(of: newWindow)
}
}
.store(in: &cancellables)
}
/// This function uses notifications to track closing of `window`
private func monitorClosing(of window: NSWindow) {
NotificationCenter.default
.publisher(for: NSWindow.willCloseNotification, object: window)
.sink { [weak self] notification in
guard let self = self else { return }
self.onChange(nil)
self.cancellables.removeAll()
}
.store(in: &cancellables)
}
}
}
This implementation can then be used to get a handle to NSWindow as soon as possible. The issue we still face: we don't have full control of the window. We are just monitoring what happens and can interact with the NSWindow instance. This means: we can set the position, but we don't know exactly at which instant this should happen. E.g. setting the windows frame directly after the view has been added to the window, will have no impact as SwiftUI is first doing layout calculations to decide afterwards where it will place the window.
After some fiddling around, I started tracking the NSWindow.isVisible property. This allows me to set the position whenever the window becomes visible. Using above WindowAccessor my ContentView implementation looks as follows:
import SwiftUI
import Combine
let WIDTH: CGFloat = 400
let HEIGHT: CGFloat = 200
struct ContentView: View {
#State var window : NSWindow?
#State private var cancellables = Set<AnyCancellable>()
var body: some View {
VStack {
Text("it finally works!")
.font(.largeTitle)
Text(window?.frameAutosaveName ?? "-")
}
.frame(width: WIDTH, height: HEIGHT, alignment: .center)
.background(WindowAccessor { newWindow in
if let newWindow = newWindow {
monitorVisibility(window: newWindow)
} else {
// window closed: release all references
self.window = nil
self.cancellables.removeAll()
}
})
}
private func monitorVisibility(window: NSWindow) {
window.publisher(for: \.isVisible)
.dropFirst() // we know: the first value is not interesting
.sink(receiveValue: { isVisible in
if isVisible {
self.window = window
placeWindow(window)
}
})
.store(in: &cancellables)
}
private func placeWindow(_ window: NSWindow) {
let main = NSScreen.main!
let visibleFrame = main.visibleFrame
let windowSize = window.frame.size
let windowX = visibleFrame.midX - windowSize.width/2
let windowY = visibleFrame.midY - windowSize.height/2
let desiredOrigin = CGPoint(x: windowX, y: windowY)
window.setFrameOrigin(desiredOrigin)
}
}
I hope this solution helps others who want to get more control to the window in SwiftUI.

SwiftUI - disclosure group expanding behaviour - keep top item static

I'm trying to create a drop down menu using swift UI. I'm actually implementing this into a UIKit project.
The basic functionality I am going for should be that the user clicks on a label with a certain data unit on it and a list expands with other units of measure that can be selected.
Here is my SwiftUI code:
import SwiftUI
#available(iOS 14.0.0, *)
struct DataUnitDropDown: View {
var units = ["ltr", "usg", "impg"]
#State private var selectedDataUnit = 0
#State private var isExpanded = false
var body: some View {
VStack(alignment: .leading, spacing: 15) {
DisclosureGroup(units[selectedDataUnit], isExpanded: $isExpanded) {
VStack {
ForEach(0 ..< units.count, id: \.self) { index in
Text("\(units[index])")
.font(.title3)
.padding(.all)
.onTapGesture {
self.selectedDataUnit = index
withAnimation {
self.isExpanded.toggle()
}
}
}
}
}
}
}
}
So I have a couple of questions here. Firstly, ideally I wanted to place this into an existing horizontal UIStackView. However, the issue I have is that obviously once the dropdown expands, it increases the height of the stack, which is not what I want. Here is a screen shot of the component (see 'ltr'):
When expanded in the stackView:
So I reluctantly placed it in the main view adding some autolayout constraints:
if #available(iOS 14.0.0, *) {
let controller = UIHostingController(rootView: DataUnitDropDown())
controller.view.translatesAutoresizingMaskIntoConstraints = false
addSubview(controller.view)
controller.view.leadingAnchor.constraint(equalTo: refuelInfoView.actualUpliftVolume.trailingAnchor).isActive = true
controller.view.centerYAnchor.constraint(equalTo: refuelInfoView.actualUpliftVolume.centerYAnchor).isActive = true
}
But now when I expand, the whole menu shifts up:
Clearly not what I am looking for.
So I suppose my question is 2-fold:
1- Is there any way I can incorporate this element into an existing stack view without the stack increasing in height when the dropdown is expanded? (I guess not!)
2- If I have to add this to the main view rather than the stack, how can I stop the entire menu shifting up when it expands?
Update:
I have amended the code as follows:
if #available(iOS 14.0.0, *) {
let controller = UIHostingController(rootView: DataUnitDropDown())
controller.view.translatesAutoresizingMaskIntoConstraints = false
controller.view.clipsToBounds = false
controller.view.heightAnchor.constraint(equalToConstant: 22).isActive = true
controller.view.topAnchor.constraint(equalTo: topAnchor).isActive = true
stackSubviews = [inputField, controller.view]
}
Now, without the topAnchor constraint this works perfectly, but I do need this constraint. But I get an error due to the views. not being in the same hierarchy
I assume instead of
controller.view.centerYAnchor.constraint(equalTo:
refuelInfoView.actualUpliftVolume.centerYAnchor).isActive = true
you need
controller.view.topAnchor.constraint(equalTo:
refuelInfoView.actualUpliftVolume.bottomAnchor).isActive = true

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
}

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