How to create DropDownMenu in SwiftUI? - ios

I need to create a DropDownMenu using SwiftUI. Finally I created a DropDownMenu where there is a problem the immediate next following siblings of this DropDownMenu's VStack moved from it's default position when it's expanded also the VStack height increase dynamically. Please find the view as following:
Actually I need a real DropDownMenu as usually we are use to in Desktop Application. Have a look of my code as following:
import UIKit
import SwiftUI
public struct DropDownMenu: View {
#State var expanded: Bool = false
public var body: some View {
let marineGreen : Color = Color(UIColor.parse(0x346C7C))
let whitishGreen: Color = Color(UIColor.parse(0xE7EEEF))
let genders: [String] = ["Male", "Female", "Unknown"]
let width : CGFloat = 140
let menu = VStack(alignment: .leading){
HStack{
Text("Gender").fontWeight(.heavy).foregroundColor(whitishGreen)
Image(systemName: self.expanded ? "chevron.up" : "chevron.down").resizable().frame(width: 13, height: 6).foregroundColor(whitishGreen)
}.onTapGesture {
self.expanded.toggle()
}.frame(width: width)
if (self.expanded) {
ForEach(0 ..< genders.count, id: \.self) {
self.menuItem(genders[$0], width, whitishGreen)
}
}
}
.padding().frame(width: width)
.background(self.expanded ? marineGreen.opacity(0.9) : marineGreen)
.animation(.none)
.onTapGesture {
self.expanded.toggle()
}
return menu
}
private func menuItem(_ gender: String, _ width: CGFloat, _ color: Color) -> some View {
let padding: CGFloat = 20
let height : CGFloat = 20
return VStack {
Divider().background(color)
Button(action: {self.expanded.toggle()}){
Text("\(gender)").frame(height: height).padding([.leading, .trailing], padding)
}
.frame(minWidth: width).contentShape(Rectangle())
.foregroundColor(color)
}
}
}
Extension of UIColor:
extension UIColor {
static func parse(_ hex: UInt32, alpha: Double = 1.0) -> UIColor {
let red = CGFloat((hex & 0xFF0000) >> 16)/256.0
let green = CGFloat((hex & 0xFF00) >> 8)/256.0
let blue = CGFloat(hex & 0xFF)/256.0
return UIColor(red: red, green: green, blue: blue, alpha: CGFloat(alpha))
}
}
I expect your help to resolve this issue and hope it would be highly appreciable too.

Check out this video, it has a nice walkthrough that should be exactly what you (or others coming to this page later) want: https://www.youtube.com/watch?v=CwD4cScGCq8

Related

Drag separators in SwiftUI

How would I add draggable separator lines between Views or UIViews using purely SwiftUI. Is it even possible with SwiftUI, or would I have to fall back on UIKit?
Example screens with separators:
I can't find this kind of stuff in the SwiftUI documentation. Even just enough info to do the top-left two-pane example would be useful.
(Similar questions have been asked here and here , but these are 5 and 7 years old, and deal with Objective-C / UIKit, not Swift / SwiftUI)
Here is a sample that allows horizontal and vertical resizing using grips. Dragging the purple grip resizes horizontally and the orange grip vertically. Both vertical and horizontal sizes are bounded by device resolution. The red pane is always visible, but the grips and other panes can be hidden using a toggle. There is also a reset button to restore, it is only visible when the original state changes. There are other tidbits that are useful and commented inline.
// Resizable panes, red is always visible
struct PanesView: View {
static let startWidth = UIScreen.main.bounds.size.width / 6
static let startHeight = UIScreen.main.bounds.size.height / 5
// update drag width when the purple grip is dragged
#State private var dragWidth : CGFloat = startWidth
// update drag height when the orange grip is dragged
#State private var dragHeight : CGFloat = startHeight
// remember show/hide green and blue panes
#AppStorage("show") var show : Bool = true
// keeps the panes a reasonable size based on device resolution
var minWidth : CGFloat = UIScreen.main.bounds.size.width / 6
let minHeight : CGFloat = UIScreen.main.bounds.size.height / 5
// purple and orange grips are this thick
let thickness : CGFloat = 9
// computed property that shows resize when appropriate
var showResize : Bool {
dragWidth != PanesView.startWidth || dragHeight != PanesView.startHeight
}
// use computed properties to keep the body tidy
var body: some View {
HStack(spacing: 0) {
redPane
// why two show-ifs? the animated one chases the non-animated and adds visual interest
if show {
purpleGrip
}
if show { withAnimation {
VStack(spacing: 0) {
greenPane
orangeGrip
Color.blue.frame(height: dragHeight) // blue pane
}
.frame(width: dragWidth)
} }
}
}
var redPane : some View {
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)) {
Color.red
// shows and hides the green and blue pane, both grips
Toggle(isOn: $show.animation(), label: {
// change icon depending on toggle position
Image(systemName: show ? "eye" : "eye.slash")
.font(.title)
.foregroundColor(.primary)
})
.frame(width: 100)
.padding()
}
}
var purpleGrip : some View {
Color.purple
.frame(width: thickness)
.gesture(
DragGesture()
.onChanged { gesture in
let screenWidth = UIScreen.main.bounds.size.width
// the framework feeds little deltas as the drag continues updating state
let delta = gesture.translation.width
// make sure drag width stays bounded
dragWidth = max(dragWidth - delta, minWidth)
dragWidth = min(screenWidth - thickness - minWidth, dragWidth)
}
)
}
var greenPane : some View {
ZStack(alignment: Alignment(horizontal: .center, vertical: .top)) {
Color.green
// reset to original size
if showResize { withAnimation {
Button(action: { withAnimation {
dragWidth = UIScreen.main.bounds.size.width / 6
dragHeight = UIScreen.main.bounds.size.height / 5
} }, label: {
Image(systemName: "uiwindow.split.2x1")
.font(.title)
.foregroundColor(.primary)
.padding()
})
.buttonStyle(PlainButtonStyle())
}}
}
}
var orangeGrip : some View {
Color.orange
.frame(height: thickness)
.gesture(
DragGesture()
.onChanged { gesture in
let screenHeight = UIScreen.main.bounds.size.height
let delta = gesture.translation.height
dragHeight = max(dragHeight - delta, minHeight)
dragHeight = min(screenHeight - thickness - minHeight, dragHeight)
}
)
}
}
I decided to do a more SwiftUI-like approach. It can be any size so it is not fixed to the whole screen size. It can be called like this:
import SwiftUI
import ViewExtractor
struct ContentView: View {
var body: some View {
SeparatedStack(.vertical, ratios: [6, 4]) {
SeparatedStack(.horizontal, ratios: [2, 8]) {
Text("Top left")
Text("Top right")
}
SeparatedStack(.horizontal) {
Text("Bottom left")
Text("Bottom middle")
Text("Bottom right")
}
}
}
}
Result:
Code (read notes below):
// MARK: Extensions
extension Array {
subscript(safe index: Int) -> Element? {
guard indices ~= index else { return nil }
return self[index]
}
}
extension View {
#ViewBuilder func `if`<Output: View>(_ condition: Bool, transform: #escaping (Self) -> Output, else: #escaping (Self) -> Output) -> some View {
if condition {
transform(self)
} else {
`else`(self)
}
}
}
// MARK: Directional layout
enum Axes {
case horizontal
case vertical
}
private struct EitherStack<Content: View>: View {
let axes: Axes
let content: () -> Content
var body: some View {
switch axes {
case .horizontal: HStack(spacing: 0, content: content)
case .vertical: VStack(spacing: 0, content: content)
}
}
}
// MARK: Stacks
struct SeparatedStack: View {
static let dividerWidth: CGFloat = 5
static let minimumWidth: CGFloat = 20
private let axes: Axes
private let ratios: [CGFloat]?
private let views: [AnyView]
init<Views>(_ axes: Axes, ratios: [CGFloat]? = nil, #ViewBuilder content: TupleContent<Views>) {
self.axes = axes
self.ratios = ratios
views = ViewExtractor.getViews(from: content)
}
var body: some View {
GeometryReader { geo in
Color.clear
.overlay(SeparatedStackInternal(views: views, geo: geo, axes: axes, ratios: ratios))
}
}
}
// MARK: Stacks (internal)
private struct SeparatedStackInternal: View {
private struct GapBetween: Equatable {
let gap: CGFloat
let difference: CGFloat?
static func == (lhs: GapBetween, rhs: GapBetween) -> Bool {
lhs.gap == rhs.gap && lhs.difference == rhs.difference
}
}
#State private var dividerProportions: [CGFloat]
#State private var lastProportions: [CGFloat]
private let views: [AnyView]
private let geo: GeometryProxy
private let axes: Axes
init(views: [AnyView], geo: GeometryProxy, axes: Axes, ratios: [CGFloat]?) {
self.views = views
self.geo = geo
self.axes = axes
// Set initial proportions
if let ratios = ratios {
guard ratios.count == views.count else {
fatalError("Mismatching ratios array size. Should be same length as number of views.")
}
let total = ratios.reduce(0, +)
var proportions: [CGFloat] = []
for index in 0 ..< ratios.count - 1 {
let ratioTotal = ratios.prefix(through: index).reduce(0, +)
proportions.append(ratioTotal / total)
}
_dividerProportions = State(initialValue: proportions)
_lastProportions = State(initialValue: proportions)
} else {
let range = 1 ..< views.count
let new = range.map { index in
CGFloat(index) / CGFloat(views.count)
}
_dividerProportions = State(initialValue: new)
_lastProportions = State(initialValue: new)
}
}
var body: some View {
EitherStack(axes: axes) {
ForEach(views.indices) { index in
if index != 0 {
Color.gray
.if(axes == .horizontal) {
$0.frame(width: SeparatedStack.dividerWidth)
} else: {
$0.frame(height: SeparatedStack.dividerWidth)
}
}
let gapAtIndex = gapBetween(index: index)
views[index]
.if(axes == .horizontal) {
$0.frame(maxWidth: gapAtIndex.gap)
} else: {
$0.frame(maxHeight: gapAtIndex.gap)
}
.onChange(of: gapAtIndex) { _ in
if let difference = gapBetween(index: index).difference {
if dividerProportions.indices ~= index - 1 {
dividerProportions[index - 1] -= difference / Self.maxSize(axes: axes, geo: geo)
lastProportions[index - 1] = dividerProportions[index - 1]
}
}
}
}
}
.overlay(overlay(geo: geo))
}
#ViewBuilder private func overlay(geo: GeometryProxy) -> some View {
ZStack {
ForEach(dividerProportions.indices) { index in
Color(white: 0, opacity: 0.0001)
.if(axes == .horizontal) { $0
.frame(width: SeparatedStack.dividerWidth)
.position(x: lastProportions[index] * Self.maxSize(axes: axes, geo: geo))
} else: { $0
.frame(height: SeparatedStack.dividerWidth)
.position(y: lastProportions[index] * Self.maxSize(axes: axes, geo: geo))
}
.gesture(
DragGesture()
.onChanged { drag in
let translation = axes == .horizontal ? drag.translation.width : drag.translation.height
let currentPosition = lastProportions[index] * Self.maxSize(axes: axes, geo: geo) + translation
let offset = SeparatedStack.dividerWidth / 2 + SeparatedStack.minimumWidth
let minPos = highEdge(of: lastProportions, index: index - 1) + offset
let maxPos = lowEdge(of: lastProportions, index: index + 1) - offset
let newPosition = min(max(currentPosition, minPos), maxPos)
dividerProportions[index] = newPosition / Self.maxSize(axes: axes, geo: geo)
}
.onEnded { drag in
lastProportions[index] = dividerProportions[index]
}
)
}
}
.if(axes == .horizontal) {
$0.offset(y: geo.size.height / 2)
} else: {
$0.offset(x: geo.size.width / 2)
}
}
private static func maxSize(axes: Axes, geo: GeometryProxy) -> CGFloat {
switch axes {
case .horizontal: return geo.size.width
case .vertical: return geo.size.height
}
}
private func gapBetween(index: Int) -> GapBetween {
let low = lowEdge(of: dividerProportions, index: index)
let high = highEdge(of: dividerProportions, index: index - 1)
let gap = max(low - high, SeparatedStack.minimumWidth)
let difference = gap == SeparatedStack.minimumWidth ? SeparatedStack.minimumWidth - low + high : nil
return GapBetween(gap: gap, difference: difference)
}
private func lowEdge(of proportions: [CGFloat], index: Int) -> CGFloat {
var edge: CGFloat { proportions[index] * Self.maxSize(axes: axes, geo: geo) - SeparatedStack.dividerWidth / 2 }
return proportions[safe: index] != nil ? edge : Self.maxSize(axes: axes, geo: geo)
}
private func highEdge(of proportions: [CGFloat], index: Int) -> CGFloat {
var edge: CGFloat { proportions[index] * Self.maxSize(axes: axes, geo: geo) + SeparatedStack.dividerWidth / 2 }
return proportions[safe: index] != nil ? edge : 0
}
}
Note: this uses my GeorgeElsham/ViewExtractor for the ability to pass in #ViewBuilder content, rather than just an array of views. This part is not necessary, however I recommend it because it makes the code readable and more SwiftUI-like.
Here is what I have been using. I have a generic SplitView with a primary (P) and secondary (V) view created using ViewBuilders. The fraction identifies the ratio of primary to secondary width or height at open. I use secondaryHidden to force the primary to full width modulo half of the visibleThickness of the Splitter. The invisibleThickness is the grabbable width/height for the Splitter. The SizePreferenceKey is used with a GeometryReader on a clear background to capture the overallSize of the SplitView so that the fraction can be applied properly.
fileprivate struct SplitView<P: View, S: View>: View {
private let layout: Layout
private let zIndex: Double
#Binding var fraction: CGFloat
#Binding var secondaryHidden: Bool
private let primary: P
private let secondary: S
private let visibleThickness: CGFloat = 2
private let invisibleThickness: CGFloat = 30
#State var overallSize: CGSize = .zero
#State var primaryWidth: CGFloat?
#State var primaryHeight: CGFloat?
var hDrag: some Gesture {
// As we drag the Splitter horizontally, adjust the primaryWidth and recalculate fraction
DragGesture()
.onChanged { gesture in
primaryWidth = gesture.location.x
fraction = gesture.location.x / overallSize.width
}
}
var vDrag: some Gesture {
// As we drag the Splitter vertically, adjust the primaryHeight and recalculate fraction
DragGesture()
.onChanged { gesture in
primaryHeight = gesture.location.y
fraction = gesture.location.y / overallSize.height
}
}
enum Layout: CaseIterable {
/// The orientation of the primary and seconday views (e.g., Vertical = VStack, Horizontal = HStack)
case Horizontal
case Vertical
}
var body: some View {
ZStack(alignment: .topLeading) {
switch layout {
case .Horizontal:
// When we init the view, primaryWidth is nil, so we calculate it from the
// fraction that was passed-in. This lets us specify the location of the Splitter
// when we instantiate the SplitView.
let pWidth = primaryWidth ?? width()
let sWidth = overallSize.width - pWidth - visibleThickness
primary
.frame(width: pWidth)
secondary
.frame(width: sWidth)
.offset(x: pWidth + visibleThickness, y: 0)
Splitter(orientation: .Vertical, visibleThickness: visibleThickness)
.frame(width: invisibleThickness, height: overallSize.height)
.position(x: pWidth + visibleThickness / 2, y: overallSize.height / 2)
.zIndex(zIndex)
.gesture(hDrag, including: .all)
case .Vertical:
// When we init the view, primaryHeight is nil, so we calculate it from the
// fraction that was passed-in. This lets us specify the location of the Splitter
// when we instantiate the SplitView.
let pHeight = primaryHeight ?? height()
let sHeight = overallSize.height - pHeight - visibleThickness
primary
.frame(height: pHeight)
secondary
.frame(height: sHeight)
.offset(x: 0, y: pHeight + visibleThickness)
Splitter(orientation: .Horizontal, visibleThickness: visibleThickness)
.frame(width: overallSize.width, height: invisibleThickness)
.position(x: overallSize.width / 2, y: pHeight + visibleThickness / 2)
.zIndex(zIndex)
.gesture(vDrag, including: .all)
}
}
.background(GeometryReader { geometry in
// Track the overallSize using a GeometryReader on the ZStack that contains the
// primary, secondary, and splitter
Color.clear
.preference(key: SizePreferenceKey.self, value: geometry.size)
.onPreferenceChange(SizePreferenceKey.self) {
overallSize = $0
}
})
.contentShape(Rectangle())
}
init(layout: Layout, zIndex: Double = 0, fraction: Binding<CGFloat>, secondaryHidden: Binding<Bool>, #ViewBuilder primary: (()->P), #ViewBuilder secondary: (()->S)) {
self.layout = layout
self.zIndex = zIndex
_fraction = fraction
_primaryWidth = State(initialValue: nil)
_primaryHeight = State(initialValue: nil)
_secondaryHidden = secondaryHidden
self.primary = primary()
self.secondary = secondary()
}
private func width() -> CGFloat {
if secondaryHidden {
return overallSize.width - visibleThickness / 2
} else {
return (overallSize.width * fraction) - (visibleThickness / 2)
}
}
private func height() -> CGFloat {
if secondaryHidden {
return overallSize.height - visibleThickness / 2
} else {
return (overallSize.height * fraction) - (visibleThickness / 2)
}
}
}
fileprivate struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
With the filePrivate SplitView in place, I use HSplitView and VSplitView as the public entry points.
/// A view containing a primary view and a secondary view layed-out vertically and separated by a draggable horizontally-oriented Splitter
///
/// The primary view is above the secondary view.
struct VSplitView<P: View, S: View>: View {
let zIndex: Double
#Binding var fraction: CGFloat
#Binding var secondaryHidden: Bool
let primary: ()->P
let secondary: ()->S
var body: some View {
SplitView(layout: .Vertical, zIndex: zIndex, fraction: $fraction, secondaryHidden: $secondaryHidden, primary: primary, secondary: secondary)
}
init(zIndex: Double = 0, fraction: Binding<CGFloat>, secondaryHidden: Binding<Bool>? = nil, #ViewBuilder primary: #escaping (()->P), #ViewBuilder secondary: #escaping (()->S)) {
self.zIndex = zIndex
_fraction = fraction
_secondaryHidden = secondaryHidden ?? .constant(false)
self.primary = primary
self.secondary = secondary
}
}
/// A view containing a primary view and a secondary view layed-out horizontally and separated by a draggable vertically-oriented Splitter
///
/// The primary view is to the left of the secondary view.
struct HSplitView<P: View, S: View>: View {
let zIndex: Double
#Binding var fraction: CGFloat
#Binding var secondaryHidden: Bool
let primary: ()->P
let secondary: ()->S
var body: some View {
SplitView(layout: .Horizontal, fraction: $fraction, secondaryHidden: $secondaryHidden, primary: primary, secondary: secondary)
}
init(zIndex: Double = 0, fraction: Binding<CGFloat>, secondaryHidden: Binding<Bool>? = nil, #ViewBuilder primary: #escaping (()->P), #ViewBuilder secondary: #escaping (()->S)) {
self.zIndex = zIndex
_fraction = fraction
_secondaryHidden = secondaryHidden ?? .constant(false)
self.primary = primary
self.secondary = secondary
}
}
The Splitter is a ZStack with a visible RoundedRectangle with visibleThickness on top of a clear Color with invisibleThickness.
/// The Splitter that separates the primary from secondary views in a SplitView.
struct Splitter: View {
private let orientation: Orientation
private let color: Color
private let inset: CGFloat
private let visibleThickness: CGFloat
private var invisibleThickness: CGFloat
enum Orientation: CaseIterable {
/// The orientation of the Divider itself.
/// Thus, use Horizontal in a VSplitView and Vertical in an HSplitView
case Horizontal
case Vertical
}
var body: some View {
ZStack(alignment: .center) {
switch orientation {
case .Horizontal:
Color.clear
.frame(height: invisibleThickness)
.padding(0)
RoundedRectangle(cornerRadius: visibleThickness / 2)
.fill(color)
.frame(height: visibleThickness)
.padding(EdgeInsets(top: 0, leading: inset, bottom: 0, trailing: inset))
case .Vertical:
Color.clear
.frame(width: invisibleThickness)
.padding(0)
RoundedRectangle(cornerRadius: visibleThickness / 2)
.fill(color)
.frame(width: visibleThickness)
.padding(EdgeInsets(top: inset, leading: 0, bottom: inset, trailing: 0))
}
}
.contentShape(Rectangle())
}
init(orientation: Orientation, color: Color = .gray, inset: CGFloat = 8, visibleThickness: CGFloat = 2, invisibleThickness: CGFloat = 30) {
self.orientation = orientation
self.color = color
self.inset = inset
self.visibleThickness = visibleThickness
self.invisibleThickness = invisibleThickness
}
}
Here's an example. One additional note is that I had to use zIndex for the Splitter when SplitViews contained other SplitViews that contain other SplitViews. This is because the because the overlap of the multiple Splitters with the primary/secondary of adjacent views prevents the drag gesture from being detected. It's not necessary to specify in simpler cases.
struct ContentView: View {
var body: some View {
HSplitView(
zIndex: 2,
fraction: .constant(0.5),
primary: { Color.red },
secondary: {
VSplitView(
zIndex: 1,
fraction: .constant(0.5),
primary: { Color.blue },
secondary: {
HSplitView(
zIndex: 0,
fraction: .constant(0.5),
primary: { Color.green },
secondary: { Color.yellow }
)
}
)
}
)
}
}
And the result...

SwiftUI: how can to calculate the width of a Text view?

I have unknown number of Text views I need to account for (as many hours that pass during sleep). Each Text view is just a single hour e.g. Text("12"), Text("1") etc.
I need to know the width of all the combined hour Text Views so I can subtract from the overall geometry to space accordingly. The below is a widthOfString function that may have worked with UIKit but doesn't seem to calculate the proper size in SwiftUI. How can I achieve this?
Example usage:
Spacer()
.frame(width: CGFloat(hourAsDate.timeIntervalSince(hoursBetweenSleepStartAndEnd[(hoursBetweenSleepStartAndEnd.firstIndex(of: hourAsDate) ?? 0) - 1]) / totalSleepSeconds) * calculateSpaceOfHourText())
Text("\(calendar.component(.hour, from: hourAsDate))")
.font(Font.system(size: 13.0))
Spacer()
.frame(width: CGFloat(sleepEndDate.timeIntervalSince(hourAsDate) / totalSleepSeconds) * calculateSpaceOfHourText())
My Helper Function and Extension:
//helper
private func calculateSpaceOfHourText() -> CGFloat {
//The idea here is to try to add up all the space that is taken up by hour Text views and subtract it from the total with of geometry
var hoursToUseForCalculationOfSpace = hoursBetweenSleepStartAndEnd
if hoursBetweenSleepStartAndEnd.first ?? Date() <= sleepStartDate {
hoursToUseForCalculationOfSpace.removeFirst()
}
if hoursBetweenSleepStartAndEnd.last ?? Date() >= sleepEndDate {
hoursToUseForCalculationOfSpace.removeLast()
}
return (proxy.size.width - hoursToUseForCalculationOfSpace.map { calendar.component(.hour, from: $0) }.map { "\($0)".widthOfString(usingFont: UIFont.systemFont(ofSize: 13, weight: .regular)) }.reduce(0, +))
}
}
extension String {
func widthOfString(usingFont font: UIFont) -> CGFloat {
let fontAttributes = [NSAttributedString.Key.font: font]
let size = self.size(withAttributes: fontAttributes)
//print("THIS width = \(size.width)")
return size.width
}
}
Not direct an answer to your problem, but maybe a direction to look into is the PreferenceKey protocol in combination with GeometryReader. For example the following code draws a RoundedRectangle in the background with the height of the view it modifies:
struct HeightPreferenceKey: PreferenceKey {
static var defaultValue: [CGFloat] = []
static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) {
value.append(contentsOf: nextValue())
}
typealias Value = [CGFloat]
}
struct HeightPreferenceViewSetter: View {
var body: some View {
GeometryReader { geometry in
RoundedRectangle(cornerRadius: 13)
.fill(Color.gray)
.preference(key: HeightPreferenceKey.self,
value: [geometry.size.height])
}
}
}
struct HeightModifier: ViewModifier {
func body(content: Content) -> some View {
content
.background(HeightPreferenceViewSetter())
}
}

Swift: How to remove green border from picker in watchos? Any Picker Styling? [duplicate]

I need to use a Picker view but I don't see any options to hide the green focus border.
Code:
#State private var selectedIndex = 0
var values: [String] = (0 ... 12).map { String($0) }
var body: some View {
Picker(selection: $selectedIndex, label: Text("")) {
ForEach(0 ..< values.count) {
Text(values[$0])
}
}
.labelsHidden()
}
The following extension puts a black overlay over the picker border.
Result
Code
extension Picker {
func focusBorderHidden() -> some View {
let isWatchOS7: Bool = {
if #available(watchOS 7, *) {
return true
}
return false
}()
let padding: EdgeInsets = {
if isWatchOS7 {
return .init(top: 17, leading: 0, bottom: 0, trailing: 0)
}
return .init(top: 8.5, leading: 0.5, bottom: 8.5, trailing: 0.5)
}()
return self
.overlay(
RoundedRectangle(cornerRadius: isWatchOS7 ? 8 : 7)
.stroke(Color.black, lineWidth: isWatchOS7 ? 4 : 3.5)
.offset(y: isWatchOS7 ? 0 : 8)
.padding(padding)
)
}
}
Usage
Make sure .focusBorderHidden() is the first modifier.
Picker( [...] ) {
[...]
}
.focusBorderHidden()
[...]
On the Picker, something like this can be added to cover up the green border.
#ScaledMetric var borderWidth: CGFloat = 5 // or it can be 3
Picker {
...
}.border(Color.black, width: borderWidth)
Adding a mask of cornerRadius of whatever required but with a padding of 2 or over (so as to mask the outer edge of the view) as the green border width on the watch tends to be around 2... will do the trick
.mask(RoundedRectangle(cornerRadius: 12).padding(2))
I like the border method from Ray Hunter too but to keep things tidy and simple I rather stay away from having lots and lots of #... variables

Drag has reversed direction

I created a Game which involves dragging tiles and dropping them, this was created in MacOS. I then re built in IOS exactly the same application, and it uses UIKit as expected. The only issue i have is that when i drag the tiles they are going in the opposite vertical direction.
So i drag up and left, and the tile goes down and left... but when i release, it knows where i am dropping and the tile appears in the correct place.
import SwiftUI
enum DragState {
case unknown
case good
case bad
}
struct Letter: View {
#State private var dragAmount = CGSize.zero
#State private var dragState = DragState.unknown
var text: String
var index: Int
var onChanged: ((CGPoint, String) -> DragState)?
var onEnded: ((CGPoint, Int, String) -> Void)?
var body: some View {
Image(text)
.frame(width: 67, height: 97)
.offset(dragAmount)
.zIndex(dragAmount == .zero ? 0 : 1)
.shadow(color: dragColor, radius: dragAmount == .zero ? 0 : 10)
.shadow(color: dragColor, radius: dragAmount == .zero ? 0 : 10)
.gesture(
DragGesture(coordinateSpace: .global)
.onChanged {
self.dragAmount = CGSize(width: $0.translation.width, height: -$0.translation.height)
self.dragState = self.onChanged?($0.location, self.text) ?? .unknown
}
.onEnded {
if self.dragState == .good {
self.onEnded?($0.location, self.index, self.text)
}
self.dragAmount = .zero
}
)
}
var dragColor: Color {
switch dragState {
case .unknown:
return .black
case .good:
return .green
case .bad:
return .red
}
}
}
So it's just whilst dragging that the tile is visually incorrect.
Is this due to change from Cocoa to UIKit perhaps, and can i therefore change something to solve this?
Many Thanks in Advance
I found it, it was simply the line of code
self.dragAmount = CGSize(width: $0.translation.width, height: -$0.translation.height)
needed changing to
self.dragAmount = CGSize(width: $0.translation.width, height: $0.translation.height)
Sometimes its stares you in the face
Thanks

SwiftUI access a view created in a ForEach loop

Is there a way to access a view created in a ForEach loop? I'm creating views (Rectangles) with this struct on this loop. I want to change the fill color of the rects upon the tapped gestures.
struct DisplayingRect:Identifiable {
var id = UUID()
var width:CGFloat = 0
var height:CGFloat = 0
var xAxis:CGFloat = 0
var yAxis:CGFloat = 0
init(width:CGFloat, height:CGFloat, xAxis:CGFloat, yAxis:CGFloat) {
self.width = width
self.height = height
self.xAxis = xAxis
self.yAxis = yAxis
}
}
ForEach(self.rects) { rect in
Rectangle()
.fill(Color.init(.sRGB, red: 1, green: 0, blue: 0, opacity: 0.2))
.frame(width: rect.width, height: rect.height)
.offset(x: rect.xAxis, y: rect.yAxis)
.id(rect.id)
.onTapGesture {
print("Clicked")
self.rectTapped = rect.width
print(rect.width)
print(rect.id)
if !self.didTap {
self.didTap = true
} else {
self.didTap = false
}
}
I can assign each view with an id setting its id property, but I don't know where they are stored or how to modify them upon the click. I can create function that returns a view (Rectangle) and store them in an array, and display them in the screen, but again I don't know how to access them and modify the one I want.
Keep a #State to track which indices are highlighted then make your color a function of that state. Here is an example with animation:
struct ContentView: View {
#State private var selectedIndices = Set<Int>()
var body: some View {
ForEach (0..<3) { index in
Color(self.selectedIndices.contains(index) ? .yellow : .blue)
.frame(width: 200, height: 200)
.animation(.easeInOut(duration: 0.25))
.onTapGesture {
if self.selectedIndices.contains(index) {
self.selectedIndices.remove(index)
} else {
self.selectedIndices.insert(index)
}
}
}
}
}
you can do it like this:
struct DisplayingRect:Identifiable, Hashable {
static var counter = 0
var id : Int = DisplayingRect.counter
var width:CGFloat = 0
var height:CGFloat = 0
var xAxis:CGFloat = 0
var yAxis:CGFloat = 0
var color: Color = Color.red
init(width:CGFloat, height:CGFloat, xAxis:CGFloat, yAxis:CGFloat) {
self.width = width
self.height = height
self.xAxis = xAxis
self.yAxis = yAxis
DisplayingRect.counter = DisplayingRect.counter + 1
}
}
struct ContentView : View {
#State var rects : [DisplayingRect] = [
DisplayingRect(width: 30, height: 30, xAxis: 0, yAxis: 0),
DisplayingRect(width: 50, height: 50, xAxis: 50, yAxis: 50)
]
func setColorToID(_ id: Int) {
rects[id].color = Color.blue
}
var body: some View {
ForEach(self.rects, id: \.self) { rect in
Rectangle()
.fill(rect.color)
.frame(width: rect.width, height: rect.height)
.offset(x: rect.xAxis, y: rect.yAxis)
.id(rect.id)
.onTapGesture {
print(rect.id)
self.setColorToID(rect.id)
}
}
}
}
SwiftUI encourages a declarative approach – you shouldn't need to (and in fact can't) access any view directly to store a reference to it. Your views can be given data, and whenever that data changes, they'll update.
In this case, you could have your DisplayingRect store a color property, then have the tap gesture on each Rectangle look up the right struct in your rects array by ID, and modify the color property.
To separate out the logic from your view and make more of this unit testable, you might want to create some kind of view model class that encompasses this, but putting it all inside your view would work without these benefits.
This approach could look something like this (test locally & working):
struct DisplayingRect: Identifiable {
let id = UUID()
var color = Color.red
var width: CGFloat
var height: CGFloat
var xAxis: CGFloat
var yAxis: CGFloat
init(
width: CGFloat,
height: CGFloat,
xAxis: CGFloat = 0,
yAxis: CGFloat = 0)
{
self.width = width
self.height = height
self.xAxis = xAxis
self.yAxis = yAxis
}
}
final class ContentViewModel: ObservableObject {
#Published
private(set) var rects: [DisplayingRect] = [
.init(width: 100, height: 100),
.init(width: 100, height: 100),
.init(width: 100, height: 100)
]
func didTapRectangle(id: UUID) {
guard let rectangleIndex = rects.firstIndex(where: { $0.id == id }) else {
return
}
rects[rectangleIndex].color = .blue
}
}
struct ContentView: View {
#ObservedObject
var viewModel = ContentViewModel()
var body: some View {
VStack {
ForEach(viewModel.rects) { rect in
Rectangle()
.fill(rect.color)
.frame(width: rect.width, height: rect.height)
.offset(x: rect.xAxis, y: rect.yAxis)
.onTapGesture {
self.viewModel.didTapRectangle(id: rect.id)
}
}
}
}
}
In this case, the #ObservedObject property wrapper along with ObservableObject protocol allow the view to update itself whenever data it uses from viewModel is changed. To automatically signal properties that should cause the view to refresh, the #Published property wrapper is used.
https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-observedobject-to-manage-state-from-external-objects

Resources