SwiftUI custom alignment pushes views wider than parent - alignment

I start with this. Some UI with two sliders.
Defined by this code:
struct ContentView: View {
#State private var num1: Double = 0.5
#State private var num2: Double = 0.5
let blueGreen = Color(red: 0.2, green: 0.6, blue: 0.6)
var body: some View {
VStack {
Circle().fill(blueGreen).border(Color.blue, width: 1.0).padding(4.0)
VStack {
HStack {
Text("Value:")
Slider(value: $num1, in: 0...1)
}
HStack {
Text("Opacity:")
Slider(value: $num2, in: 0...1)
}
}
Spacer()
}.border(Color.green, width: 1.0).padding()
}
}
I want the "Value:" and "Opacity:" labels to be aligned on their trailing edge, leaving the sliders lined-up and the same width. I've heard that custom alignments can align Views like this, Views that are not siblings.
So I add the custom alignment:
extension HorizontalAlignment {
private enum MyAlignment : AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.trailing]
}
}
static let myAlignment = HorizontalAlignment(MyAlignment.self)
}
struct ContentView: View {
#State private var num1: Double = 0.5
#State private var num2: Double = 0.5
let blueGreen = Color(red: 0.2, green: 0.6, blue: 0.6)
var body: some View {
VStack {
Circle().fill(blueGreen).border(Color.blue, width: 1.0).padding(4.0)
VStack(alignment: .myAlignment) {
HStack {
Text("Value:").alignmentGuide(.myAlignment) { d in d[.trailing] }
Slider(value: $num1, in: 0...1)
}
HStack {
Text("Opacity:").alignmentGuide(.myAlignment) { d in d[.trailing] }
Slider(value: $num2, in: 0...1)
}
}
Spacer()
}.border(Color.green, width: 1.0).padding()
}
}
It partly works. The trailing edges of the labels are aligned, but now the UI has moved partly off screen to the right.
With UIKit and auto layout, I would constrain the trailing edges of those two labels to be equal. That would not push anything off screen (assuming things were constrained to screen edges, as usual). What is going wrong here? And how do you achieve this with alignments, or by some better way, with SwiftUI?

One way to do it, is using Preferences (learn more here).
struct ContentView: View {
#State private var num1: Double = 0.5
#State private var num2: Double = 0.5
#State private var sliderWidth: CGFloat = 0
private let spacing: CGFloat = 5
let blueGreen = Color(red: 0.2, green: 0.6, blue: 0.6)
var body: some View {
VStack {
Circle().fill(blueGreen).border(Color.blue, width: 1.0).padding(4.0)
VStack(alignment: .trailing) {
HStack(spacing: self.spacing) {
Text("Value:")
.fixedSize()
.anchorPreference(key: MyPrefKey.self, value: .bounds, transform: { [$0] })
Slider(value: $num1, in: 0...1)
.frame(width: sliderWidth)
}
.frame(maxWidth: .infinity, alignment: .trailing)
HStack(spacing: self.spacing) {
Text("Opacity:")
.fixedSize()
.anchorPreference(key: MyPrefKey.self, value: .bounds, transform: { [$0] })
Slider(value: $num2, in: 0...1)
.frame(width: sliderWidth)
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
.backgroundPreferenceValue(MyPrefKey.self) { prefs -> GeometryReader<AnyView> in
GeometryReader { proxy -> AnyView in
let vStackWidth = proxy.size.width
let maxAnchor = prefs.max {
return proxy[$0].size.width < proxy[$1].size.width
}
DispatchQueue.main.async {
if let a = maxAnchor {
self.sliderWidth = vStackWidth - (proxy[a].size.width + self.spacing)
}
}
return AnyView(EmptyView())
}
}
Spacer()
}.border(Color.green, width: 1.0).padding(20)
}
}
struct MyPrefKey: PreferenceKey {
typealias Value = [Anchor<CGRect>]
static var defaultValue: [Anchor<CGRect>] = []
static func reduce(value: inout [Anchor<CGRect>], nextValue: () -> [Anchor<CGRect>]) {
value.append(contentsOf: nextValue())
}
}

Well, I agree that Apple seems forgot to give us something in SwiftUI with/from autolayout engine, like content hugging priority, resist compression, equal sizes, etc...
In SwiftUI alignment guides does not change view sizes they just shift views "as is" aligning to specified anchor points (expanding container if it is not limited in size, because by default it tights to content, that is what you see). So to solve considered situation it needs somehow to limit (or change) sizes.
The code below is not complete solution (it does not take into account possible l10n), but shows direction - as Slider does not have default size, it should be limited some how to not expand container.
I would consider below as workaround, anyway... (calculations of width might be more accurate and complex, like get frame of Text, eg. via anchor preferences, and then subtract from available geometry proxy width taking into account padding, but that is still the same - specify view frame width explicitly).
struct HueSaturationView : View {
#State private var num1: Double = 0.5
#State private var num2: Double = 0.5
let blueGreen = Color(red: 0.2, green: 0.6, blue: 0.6)
var body: some View {
GeometryReader { gp in
VStack {
Circle().fill(self.blueGreen).border(Color.blue, width: 1.0).padding(4.0)
VStack(alignment: .myAlignment) {
HStack {
Text("Value:").alignmentGuide(.myAlignment) { d in d[.trailing] }
Slider(value: self.$num1, in: 0...1)
.frame(width: gp.size.width * 2/3)
}
HStack {
Text("Opacity:").alignmentGuide(.myAlignment) { d in d[.trailing] }
Slider(value: self.$num2, in: 0...1)
.frame(width: gp.size.width * 2/3)
}
}
Spacer()
}
.border(Color.green, width: 1.0).padding()
}
}
}

Related

How to remove vertical safeAreaInsets while close gesture is active?

Here's a main ContentView and DetailedCardView and when open DetailedCardView there's a ScrollView with more content. It also have closeGesture which allow to close the card when swiping from left to right and vice versa. So, at that point problems start to appear, like this ones inside DetailedCardView:
When scrolling in ScrollView then safeAreaInsets .top and .bottom start to be visible and as a result it's shrink the whole view vertically.
The closeGesture and standard ScrollView gesture while combined is not smooth and I'd like to add closing card not only when swiping horizontally but vertically too.
As a result I'd like to have the same gesture behaviour like opening and closing detailed card in App Store App Today section. Would love to get your help 🙌
ContentView DetailedCardView closeGesture in Action safeAreaInsets
struct ContentView: View {
#State private var showCardView = false
let colors: [Color] = [.red, .purple, .yellow, .green, .blue, .mint, .orange]
#State private var selectedCardNumber = 0
var body: some View {
ZStack {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(1..<101) { number in
colors[number % colors.count]
.overlay(Text("\(number)").font(.largeTitle.bold()).foregroundColor(.white))
.frame(height: 300)
.cornerRadius(30)
.padding(10)
.onTapGesture {
showCardView.toggle()
selectedCardNumber = number
}
}
}
}
.opacity(showCardView ? 0 : 1)
.animation(.spring(), value: showCardView)
if showCardView {
CardDetailView(showCardView: $showCardView, number: selectedCardNumber)
}
}
}
}
struct CardDetailView: View {
#Binding var showCardView: Bool
#State private var cardPosition: CGSize = .zero
let number: Int
var body: some View {
ScrollView {
RoundedRectangle(cornerRadius: 30, style: .continuous)
.fill(.green)
.overlay(Text("\(number)").font(.system(size: 100).bold()).foregroundColor(.white))
.frame(height: 500)
Color.red.opacity(0.8)
.frame(height: 200)
Color.gray.opacity(0.8)
.frame(height: 200)
Color.blue
.frame(height: 200)
Color.yellow
.frame(height: 200)
}
.scaleEffect(gestureScale())
.gesture(closeGesture)
}
private var closeGesture: some Gesture {
DragGesture()
.onChanged { progress in
withAnimation(.spring()) {
cardPosition = progress.translation
}
}
.onEnded { _ in
withAnimation(.spring()) {
let positionInTwoDirections = abs(cardPosition.width)
if positionInTwoDirections > 100 {
showCardView = false
}
cardPosition = .zero
}
}
}
private func gestureScale() -> CGFloat {
let max = UIScreen.main.bounds.width / 2
let currentAmount = abs(cardPosition.width)
let percentage = currentAmount / max
return 1 - min(percentage, 0.5)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

SwiftUI - How to vertically align a Text view with another Text view to make something similar to a Table with proper alignment?

So I basically need to make a table similar to this in SwiftUI:
As you can see on the screenshot, Im having a hard time aligning the fields to their corresponding columns, as in "John" should be exactly centered to "Name" and "EUR" should be centered to "Currency", etc. As you can see on the code, Im currently eye-balling it with the stacks spacing parameter, but thats just plain awful and doesnt even work on all devices plus data is dynamic and comes from an API call so it just wont work. How can I align this on SwiftUI? I need to support iOS 13.
Current code:
struct ContentView: View {
var body: some View {
VStack(spacing: 0) {
ZStack {
TitleBackground()
Text("Clients")
.foregroundColor(.white)
}
HStack(spacing: 30) {
Text("Name")
Text("Balance")
Text("Currency")
Spacer()
}
.foregroundColor(Color(#colorLiteral(red: 0.4783782363, green: 0.4784628153, blue: 0.4783664346, alpha: 1)))
.frame(maxWidth:.infinity)
.padding(12)
.background(Color(#colorLiteral(red: 0.9332349896, green: 0.9333916306, blue: 0.9332130551, alpha: 1)))
RowView()
RowView()
RowView()
RowView()
}
}
}
struct RowView : View {
var body: some View {
HStack(spacing: 30) {
Text("John")
Text("$5300")
Text("EUR")
Spacer()
}
.padding(12)
}
}
struct TitleBackground: View {
var body: some View {
ZStack(alignment: .bottom){
Rectangle()
.frame(maxWidth: .infinity, maxHeight: 60)
.foregroundColor(.red)
.cornerRadius(15)
//We only need the top corners rounded, so we embed another rectangle to the bottom.
Rectangle()
.frame(maxWidth: .infinity, maxHeight: 15)
.foregroundColor(.red)
}
}
}
I see that Yrb posted a similar answer before me, but I'm going to post this anyway as my solution is a bit more modular/composable than theirs.
You want this layout:
We're going to implement an API so that we can get your layout with this code for the table view:
enum ColumnId: Hashable {
case name
case balance
case currency
}
struct TableView: View {
var body: some View {
WidthPreferenceDomain {
VStack(spacing: 0) {
HStack(spacing: 30) {
Text("Name")
.widthPreference(ColumnId.name)
Text("Balance")
.widthPreference(ColumnId.balance)
Text("Currency")
.widthPreference(ColumnId.currency)
Spacer()
}
.frame(maxWidth:.infinity)
.padding(12)
.background(Color(#colorLiteral(red: 0.9332349896, green: 0.9333916306, blue: 0.9332130551, alpha: 1)))
ForEach(0 ..< 4) { _ in
RowView()
}
}
}
}
}
In the code above:
I've defined a ColumnId type to identify each table column.
I've inserted a WidthPreferenceDomain container view, which we will implement below.
I've added a widthPreference modifier to each table row, which we will implement below.
We also need to use the widthPreference modifier in RowView like this:
struct RowView : View {
var body: some View {
HStack(spacing: 30) {
Text("John")
.widthPreference(ColumnId.name)
Text("$5300")
.widthPreference(ColumnId.balance)
Text("EUR")
.widthPreference(ColumnId.currency)
Spacer()
}
.padding(12)
}
}
I've placed the full code for this answer in a gist for easy copying.
So, how do we implement WidthPreferenceDomain and widthPreference?
We need to measure the width of all the items in the Name column, including the Name title itself, so that we can then set all of the Name column's items to have the same width. We need to repeat the procedure for the Balance and Currency columns.
To measure the items, we can use GeometryReader. Because GeometryReader is a greedy view that expands to fill as much space as its parent offers, we don't want to wrap any table items in a GeometryReader. Instead, we want to put a GeometryReader in a background on each table item. A background view is given the same frame as its parent.
To collect the measured widths, we can use a “preference“ by defining a type conforming to the PreferenceKey protocol. Since we want to collect the widths of all the columns, we'll store the widths in a Dictionary, keyed by a column id. To merge two widths for the same column, we take the maximum of the widths, so that each column expands to fit all its items.
fileprivate struct WidthPreferenceKey: PreferenceKey {
static var defaultValue: [AnyHashable: CGFloat] { [:] }
static func reduce(value: inout [AnyHashable : CGFloat], nextValue: () -> [AnyHashable : CGFloat]) {
value.merge(nextValue(), uniquingKeysWith: { max($0, $1) })
}
}
To distribute the collected widths back down to the table items, we can use the “environment” by defining a type conforming to the EnvironmentKey protocol. EnvironmentKey only has one requirement, a static var defaultValue property, which matches the PreferenceKey's defaultValue property, so we can re-use the WidthPreferenceKey type we already defined:
extension WidthPreferenceKey: EnvironmentKey { }
To use the environment, we also need to add a property to EnvironmentValues:
extension EnvironmentValues {
fileprivate var widthPreference: WidthPreferenceKey.Value {
get { self[WidthPreferenceKey.self] }
set { self[WidthPreferenceKey.self] = newValue }
}
}
Now we can define the widthPreference modifier:
extension View {
public func widthPreference<Domain: Hashable>(_ key: Domain) -> some View {
return self.modifier(WidthPreferenceModifier(key: key))
}
}
fileprivate struct WidthPreferenceModifier: ViewModifier {
#Environment(\.widthPreference) var widthPreference
var key: AnyHashable
func body(content: Content) -> some View {
content
.fixedSize(horizontal: true, vertical: false)
.background(GeometryReader { proxy in
Color.clear
.preference(key: WidthPreferenceKey.self, value: [key: proxy.size.width])
})
.frame(width: widthPreference[key])
}
}
This modifier measures the modified view's width using a background GeometryReader and stores it in the preference. It also sets the view's width, if the widthPreference from the environment has a width for it. Note that widthPreference[key] will be nil on the first layout pass, but that's okay, because frame(width: nil) is an “inert” modifier; it has no effect.
And now we can implement WidthPreferenceDomain. It needs to receive the preference and store it in the environment:
public struct WidthPreferenceDomain<Content: View>: View {
#State private var widthPreference: WidthPreferenceKey.Value = [:]
private var content: Content
public init(#ViewBuilder content: () -> Content) {
self.content = content()
}
public var body: some View {
content
.environment(\.widthPreference, widthPreference)
.onPreferenceChange(WidthPreferenceKey.self) { widthPreference = $0 }
}
}
There are two ways you could solve this. First, you could use two LazyVGrids with the same column set up, but you would have to fix some of your sizes. The second way isYou can solve this with with PreferenceKeys.
LazyVGrid:
struct LazyVGridView: View {
let columns = [
GridItem(.fixed(100)),
GridItem(.fixed(100)),
GridItem(.adaptive(minimum: 180))
]
var body: some View {
VStack {
LazyVGrid(columns: columns) {
Text("Name")
.padding()
Text("Balance")
.padding()
Text("Currency")
.padding()
}
.foregroundColor(Color(#colorLiteral(red: 0.4783782363, green: 0.4784628153, blue: 0.4783664346, alpha: 1)))
.background(Color(#colorLiteral(red: 0.9332349896, green: 0.9333916306, blue: 0.9332130551, alpha: 1)))
LazyVGrid(columns: columns) {
ForEach(clients) { client in
Text(client.name)
.padding()
Text(client.balance.description)
.padding()
Text(client.currency)
.padding()
}
}
}
}
}
`PreferenceKeys`:
struct PrefKeyAlignmentView: View {
let clients: [ClientInfo] = [
ClientInfo(name: "John", balance: 300, currency: "EUR"),
ClientInfo(name: "Jennifer", balance: 53000, currency: "EUR"),
ClientInfo(name: "Jo", balance: 30, currency: "EUR"),
ClientInfo(name: "Jill", balance: 530000, currency: "EUR")
]
#State private var col1Width: CGFloat = 100
#State private var col2Width: CGFloat = 100
#State private var col3Width: CGFloat = 110
var body: some View {
VStack {
HStack() {
Text("Name")
.padding()
// The background view takes on the size of the Text + the padding, and the GeometryReader reads that size
.background(GeometryReader { geometry in
Color.clear.preference(
//This sets the preference key value with the width of the background view
key: Col1WidthPrefKey.self,
value: geometry.size.width)
})
// This set the frame to the column width variable defined above
.frame(width: col1Width)
Text("Balance")
.padding()
.background(GeometryReader { geometry in
Color.clear.preference(
key: Col2WidthPrefKey.self,
value: geometry.size.width)
})
.frame(width: col2Width)
Text("Currency")
.padding()
.background(GeometryReader { geometry in
Color.clear.preference(
key: Col3WidthPrefKey.self,
value: geometry.size.width)
})
.frame(width: col3Width)
Spacer()
}
ForEach(clients) { client in
HStack {
Text(client.name)
.padding()
.background(GeometryReader { geometry in
Color.clear.preference(
key: Col1WidthPrefKey.self,
value: geometry.size.width)
})
.frame(width: col1Width)
Text(client.balance.description)
.padding()
.background(GeometryReader { geometry in
Color.clear.preference(
key: Col2WidthPrefKey.self,
value: geometry.size.width)
})
.frame(width: col2Width)
Text(client.currency)
.padding()
.background(GeometryReader { geometry in
Color.clear.preference(
key: Col3WidthPrefKey.self,
value: geometry.size.width)
})
.frame(width: col3Width)
Spacer()
}
}
}
//This sets the various column widths whenever a new entry as found.
//It keeps the larger of the new item's width, or the last largest width.
.onPreferenceChange(Col1WidthPrefKey.self) {
col1Width = max($0,col1Width)
}
.onPreferenceChange(Col2WidthPrefKey.self) {
col2Width = max($0,col2Width)
}
.onPreferenceChange(Col3WidthPrefKey.self) {
col3Width = max($0,col3Width)
}
}
}
struct ClientInfo: Identifiable {
let id = UUID()
let name: String
let balance: Int
let currency: String
}
private extension PrefKeyAlignmentView {
struct Col1WidthPrefKey: PreferenceKey {
static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat,
nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
struct Col2WidthPrefKey: PreferenceKey {
static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat,
nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
struct Col3WidthPrefKey: PreferenceKey {
static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat,
nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
}
I don't think using alignmentGuides would work as you would have to handle multiple views, and alignment guides only align one set of views, and not the views internally.
Both of these solutions will work. The PreferenceKeys will automatically adapt everything, but theoretically could push past the bounds of the screen with too much information. The LazyVGrid is more rigid, but could cause truncation/wrapping if the data is too long. I don't think you could use an adaptive layout for the LazyVGrid for all columns, if you have two different grids. It may be possible to mix both and set the LazyVGrid widths with PreferenceKeys, but I haven't personally tried it.
DataSource for both for testing:
struct ClientInfo: Identifiable {
let id = UUID()
let name: String
let balance: Int
let currency: String
}
let clients: [ClientInfo] = [
ClientInfo(name: "John", balance: 300, currency: "EUR"),
ClientInfo(name: "Jennifer", balance: 53000, currency: "EUR"),
ClientInfo(name: "Jo", balance: 30, currency: "EUR"),
ClientInfo(name: "Jill", balance: 530000, currency: "EUR")
]
Edit: While working on the answer, I forgot you were restricted to iOS 13. LazyVGrids aren't available. I left the LazyVGrid answer up as you could use if #available(os:) if you wanted to use it for later OS's, or for someone else looking for a more current solution.
Second Edit: I was thinking about how this view could be made more reusable, and I came up with a third way that is also iOS 13 compatible, and only uses one PreferenceKey to help set the background color on the headers. This also allows unlimited columns. I think this is the simplest way of the bunch:
struct TableView: View {
let headers: [String]
let tableData: [String:[String]]
let dataAlignment: [String:HorizontalAlignment]
let headerColor: Color
#State var headerHeight: CGFloat = 30
init(headers: [String], headerColor: Color = Color.clear, columnData: [[String]], alignment: [HorizontalAlignment]) {
self.headers = headers
self.headerColor = headerColor
if headers.count == columnData.count,
headers.count == alignment.count {
var data: [String:[String]] = [:]
var align: [String:HorizontalAlignment] = [:]
for i in 0..<headers.count {
let key = headers[i]
let value = columnData[i]
data[key] = value
align[key] = alignment[i]
}
dataAlignment = align
tableData = data
} else {
tableData = [:]
dataAlignment = [:]
}
}
var body: some View {
ScrollView {
HStack(spacing: 30) {
ForEach(headers, id: \.self) { header in
VStack {
Text(header)
.padding()
.background( GeometryReader { geometry in
Color.clear.preference(
key: HeaderHeightPrefKey.self,
value: geometry.size.height)
})
VStack(alignment: dataAlignment[header] ?? .center) {
ForEach(tableData[header] ?? [], id: \.self) { entry in
Text(entry)
}
}
}
}
}
.background(
VStack {
Rectangle()
.fill(headerColor)
.frame(height: headerHeight, alignment: .top)
Spacer()
}
)
.onPreferenceChange(HeaderHeightPrefKey.self) {
headerHeight = max($0,headerHeight)
}
}
}
}
private extension TableView {
struct HeaderHeightPrefKey: PreferenceKey {
static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat,
nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
}
And it is used like this:
TableView(headers: ["Name", "Balance", "Currency"], headerColor: Color(uiColor: .systemGray5), columnData: [
["John", "Jennifer", "Jo", "Jill"],
["300", "53000", "30", "530000"],
["EUR", "EUR", "EUR", "EUR"]
], alignment: [.center, .trailing, .center])
and produces this:

How to tease adjacent pages in PageTabView?

I am working with SwiftUI 2 and using a TabView with PageTabViewStyle.
Now, I am searching for a way to "tease" the pages adjacent to the current page like so:
Is it possible to achieve this effect with TabView and PageTabViewStyle?
I already tried to reduce the width of my TabView to be windowWidth-50. However, this did not lead to the adjacent pages being visible at the sides. Instead, this change introduced a hard vertical edge 50px left of the right window border, where new pages would slide in.
Here is a simple implementation. You can use the struct with the AnyView array or use the logic directly in your own implementation.
struct ContentView: View {
#State private var selected = 4
var body: some View {
// the trailing closure takes an Array of AnyView type erased views
TeasingTabView(selectedTab: $selected, spacing: 20) {
[
AnyView(TabContentView(title: "First", color: .yellow)),
AnyView(TabContentView(title: "Second", color: .orange)),
AnyView(TabContentView(title: "Fourth", color: .green)),
AnyView(TabContentView(title: "Fifth", color: .blue)),
AnyView(
Image(systemName: "lizard")
.resizable().scaledToFit()
.padding()
.frame(maxHeight: .infinity)
.border(.red)
)
]
}
}
}
struct TeasingTabView: View {
#Binding var selectedTab: Int
let spacing: CGFloat
let views: () -> [AnyView]
#State private var offset = CGFloat.zero
var viewCount: Int { views().count }
var body: some View {
VStack(spacing: spacing) {
GeometryReader { geo in
let width = geo.size.width * 0.7
LazyHStack(spacing: spacing) {
Color.clear
.frame(width: geo.size.width * 0.15 - spacing)
ForEach(0..<viewCount, id: \.self) { idx in
views()[idx]
.frame(width: width)
.padding(.vertical)
}
}
.offset(x: CGFloat(-selectedTab) * (width + spacing) + offset)
.animation(.easeOut, value: selectedTab)
.gesture(
DragGesture()
.onChanged { value in
offset = value.translation.width
}
.onEnded { value in
withAnimation(.easeOut) {
offset = value.predictedEndTranslation.width
selectedTab -= Int((offset / width).rounded())
selectedTab = max(0, min(selectedTab, viewCount-1))
offset = 0
}
}
)
}
//
HStack {
ForEach(0..<viewCount, id: \.self) { idx in
Circle().frame(width: 8)
.foregroundColor(idx == selectedTab ? .primary : .secondary.opacity(0.5))
.onTapGesture {
selectedTab = idx
}
}
}
}
}
}
struct TabContentView: View {
let title: String
let color: Color
var body: some View {
Text(title).font(.title)
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(color.opacity(0.4), ignoresSafeAreaEdges: .all)
.clipShape(RoundedRectangle(cornerRadius: 20))
}
}

SwiftUI - Autoscroll horizontal scrollview to show full text label

I have the following code which works perfectly. But the one issue I can not solve if that when on the of the Titles is particularly visible in the scrollview and the user click on the portion of the text that is visible, not only do I want to have the title selected I would like for the scollview to "auto scroll" so that the full title is displayed in the scrollview vs only the partial text.
import SwiftUI
struct CustomSegmentedPickerView: View {
#State private var selectedIndex = 0
private var titles = ["Round Trip", "One Way", "Multi-City", "Other"]
private var colors = [Color.red, Color.green, Color.blue, Color.yellow]
#State private var frames = Array<CGRect>(repeating: .zero, count: 4)
var body: some View {
VStack {
ScrollView(.horizontal, showsIndicators: false) {
ZStack {
HStack(spacing: 10) {
ForEach(self.titles.indices, id: \.self) { index in
Button(action: { self.selectedIndex = index }) {
Text(self.titles[index])
}.padding(EdgeInsets(top: 16, leading: 20, bottom: 16, trailing: 20)).background(
GeometryReader { geo in
Color.clear.onAppear { self.setFrame(index: index, frame: geo.frame(in: .global)) }
})
}
}
.background(
Capsule().fill(self.colors[self.selectedIndex].opacity(0.4))
.frame(width: self.frames[self.selectedIndex].width,
height: self.frames[self.selectedIndex].height, alignment: .topLeading)
.offset(x: self.frames[self.selectedIndex].minX - self.frames[0].minX)
, alignment: .leading
)
}
.animation(.default)
.background(Capsule().stroke(Color.gray, lineWidth: 3))
Picker(selection: self.$selectedIndex, label: Text("What is your favorite color?")) {
ForEach(0..<self.titles.count) { index in
Text(self.titles[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
Text("Value: \(self.titles[self.selectedIndex])")
Spacer()
}
}
}
func setFrame(index: Int, frame: CGRect) {
self.frames[index] = frame
}
}
struct CustomSegmentedPickerView_Previews: PreviewProvider {
static var previews: some View {
CustomSegmentedPickerView()
}
}
try this:
struct ScrollText: View
{
#State var scrollText: Bool = false
var body: some View
{
let textWidth: CGFloat = 460
let titleWidth: CGFloat = 240.0
let titleHeight: CGFloat = 70.0
HStack
{
ScrollView(.horizontal)
{
Text("13. This is my very long text title for a tv show")
.frame(minWidth: titleWidth, minHeight: titleHeight, alignment: .center)
.offset(x: (titleWidth < textWidth) ? (scrollText ? (textWidth * -1) - (titleWidth / 2) : titleWidth ) : 0, y: 0)
.animation(Animation.linear(duration: 10).repeatForever(autoreverses: false), value: scrollText)
.onAppear {
self.scrollText.toggle()
}
}
}
.frame(maxWidth: titleWidth, alignment: .center)
}
}
Optional:
if you want to calculate the text width (and not use a constant) then you can use a GeometryReader (that reads the actual dimensions of rendered objects at real time)
or you can use UIKit to calculate the width (i use this method and it is very reliant and can achieve calculations before rendering has occurred)
add this extension to your code:
// String+sizeUsingFont.swift
import Foundation
import UIKit
import SwiftUI
extension String
{
func sizeUsingFont(fontSize: CGFloat, weight: Font.Weight) -> CGSize
{
var uiFontWeight = UIFont.Weight.regular
switch weight {
case Font.Weight.heavy:
uiFontWeight = UIFont.Weight.heavy
case Font.Weight.bold:
uiFontWeight = UIFont.Weight.bold
case Font.Weight.light:
uiFontWeight = UIFont.Weight.light
case Font.Weight.medium:
uiFontWeight = UIFont.Weight.medium
case Font.Weight.semibold:
uiFontWeight = UIFont.Weight.semibold
case Font.Weight.thin:
uiFontWeight = UIFont.Weight.thin
case Font.Weight.ultraLight:
uiFontWeight = UIFont.Weight.ultraLight
case Font.Weight.black:
uiFontWeight = UIFont.Weight.black
default:
uiFontWeight = UIFont.Weight.regular
}
let font = UIFont.systemFont(ofSize: fontSize, weight: uiFontWeight)
let fontAttributes = [NSAttributedString.Key.font: font]
return self.size(withAttributes: fontAttributes)
}
}
and use it like this:
let textWidth: CGFloat = "13. This is my very long text title for a tv show".sizeUsingFont(fontSize: 24, weight: Font.Weight.regular).width
of course put the text in a var and change the font size and weight to meet your needs
also if you are about to use this solution inside a button's label, i suggest putting the onAppear() code inside async call, see this answer:
aheze spot-on answer

SwiftUI: How to pass a View as a parameter into another View to get Bottom Sheet?

I'm trying to create a View for the bottom sheet. It will have some view with a button when we tap on the button a bottom sheet has to appear.
I can modify the bottom sheet's colour, height, corner radius, how much the background view has to blur.
struct BottomSheetView: View {
#Binding var showBottomSheet: Bool
#Binding var bgColor: Color
#Binding var cornerRadius: CGFloat
#Binding var bottomSheetRatio: CGFloat
#Binding var blurPoint: CGFloat
var body: some View {
GeometryReader { geometry in
ZStack {
VStack {
Button(action: {
self.showBottomSheet.toggle()
}){
Text("click here")
.padding()
}
Spacer()
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.blur(radius: self.showBottomSheet ? self.blurPoint : 0)
.onTapGesture {
if self.showBottomSheet {
self.showBottomSheet = false
}
}
Rectangle()
.fill(self.bgColor)
.cornerRadius(self.cornerRadius)
.offset(y: self.showBottomSheet ? geometry.size.height * self.bottomSheetRatio : geometry.size.height + 200)
.animation(.spring())
}
}
}
}
In the above code
VStack {
Button(action: {
self.showBottomSheet.toggle()
}){
Text("click here")
.padding()
}
Spacer()
}
This VStack should replace with some other View, from that View I want to pass the Binding values.
Is there a way to achieve that? Thanks in advance.
I have achieved the requirement with ViewModifier
First I have created a ViewModifier struct. which will have all the required values in init()
struct BottomSheetModifier: ViewModifier {
var showBottomSheet: Bool
var blurPoint: CGFloat
var bgColor: Color
var cornerRadius: CGFloat
var bottomSheetRatio: CGFloat
init(showBottomSheet: Bool, blurPoint: CGFloat, bgColor: Color, cornerRadius: CGFloat, bottomSheetRatio: CGFloat) {
self.showBottomSheet = showBottomSheet
self.blurPoint = blurPoint
self.bgColor = bgColor
self.cornerRadius = cornerRadius
self.bottomSheetRatio = bottomSheetRatio
}
func body(content: Content) -> some View {
GeometryReader { geometry in
ZStack {
content
.blur(radius: self.showBottomSheet ? self.blurPoint : 0)
RoundedRectangle(cornerRadius: self.cornerRadius)
.fill(self.bgColor)
.offset(y: self.showBottomSheet ? geometry.size.height * self.bottomSheetRatio : geometry.size.height + 200)
.animation(.spring())
}
}
}
}
Second I've created an extension for View which will have a function with all the values as arguments and initialized the modifier in it.
extension View {
func bottomSheet(showBottomSheet: Bool, blurPoint: CGFloat, bgColor: Color, cornerRadius: CGFloat, bottomSheetRatio: CGFloat) -> some View {
modifier(BottomSheetModifier(showBottomSheet: showBottomSheet, blurPoint: blurPoint, bgColor: bgColor, cornerRadius: cornerRadius, bottomSheetRatio: bottomSheetRatio))
}
}
Third I've created a View. In that I have added some elements and simply call the modifier here with my required bottomsheet size, colour and other values.
struct DemoBottomSheetView: View {
#Binding var showBottomSheet: Bool
var body: some View {
VStack(spacing: 20) {
Text("welcome to bottom sheet")
Button(action: {
self.showBottomSheet.toggle()
}){
Text("Click Me")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
Spacer()
}
.onTapGesture {
if self.showBottomSheet {
self.showBottomSheet = false
}
}
.bottomSheet(showBottomSheet: self.showBottomSheet, blurPoint: 4, bgColor: .red, cornerRadius: 25, bottomSheetRatio: 0.5)
}
}
Final output
struct BottomSheetView<Content: View>: View {
var content: Content
...
var body: some View {
content //do anything you want with content
}
}
and you need to pass your content in your View Ininit
like
BottomSheetView(content: Text("test"),
showBottomSheet: $test1,
bgColor: $test2,
cornerRadius: $test3,
bottomSheetRatio: $ctest4,
blurPoint: $test5)

Resources