Customized segmented control in Swift? - ios

I would like to recreate the same effect than Pinterest:
Given that I'm new in Swift, I have three simple questions:
Is the menu a segmented control customized? or something like buttons?
How can I create this effect/animation of sliding? Is this a collectionView or something like that?
And finally, is it possible to create this with storyboard or the viewController needs to be created in full code ?

my two cents for highly customisable segmented, with colors and fonts.
struct ContentView: View {
#State private var segmentedSelection = 0
let titles = ["AA", "BB", "CC"]
let colors = [Color.red, .green, .white]
var body: some View {
VStack {
CustomSegmentedControl(segmentedSelection: $segmentedSelection, titles: titles, colors: colors)
Spacer()
Text("Hello, selection is " + titles[segmentedSelection] )
.padding()
}
}
}
struct CustomSegmentedControl: View {
#Binding var segmentedSelection : Int
var titles : [String]
let colors : [Color]
var body: some View {
VStack {
HStack{
ForEach(0 ..< titles.count, id: \.self){ (i: Int) in
Button(action: {
print(titles[i])
segmentedSelection = i
}, label: {
Text(titles[i])
.foregroundColor(colors[i])
.font(.system(size: i == segmentedSelection ? 22 : 16))
})
.frame(minWidth: 0, maxWidth: .infinity)
}
}
}
}
}

Related

Odd animation with .matchedGeometryEffect and NavigationView

I'm working on creating a view which uses matchedGeometryEffect, embedded within a NavigationView, and when presenting the second view another NavigationView.
The animation "works" and it matches correctly, however when it toggles from view to view it happens to swing as if unwinding from the navigation stack.
However, if I comment out the NavigationView on the secondary view the matched geometry works correctly.
I am working on iOS 14.0 and above.
Sample code
Model / Mock Data
struct Model: Identifiable {
let id = UUID().uuidString
let icon: String
let title: String
let account: String
let colour: Color
}
let mockItems: [Model] = [
Model(title: "Test title 1", colour: .gray),
Model(title: "Test title 2", colour: .blue),
Model(title: "Test title 3", colour: .purple)
...
]
Card View
struct CardView: View {
let item: Model
var body: some View {
VStack {
Text(item.title)
.font(.title3)
.fontWeight(.heavy)
}
.padding()
.frame(maxWidth: .infinity, minHeight: 100, alignment: .leading)
.background(item.colour)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
Secondary / Detail View
struct DetailView: View {
#Binding var isShowingDetail: Bool
let item: Model
let animation: Namespace.ID
var body: some View {
NavigationView { // <--- comment out here and it works
VStack {
CardView(item: item)
.matchedGeometryEffect(id: item.id, in: animation)
.onTapGesture {
withAnimation { isShowingDetail = false }
}
ScrollView(.vertical, showsIndicators: false) {
Text("Lorem ipsum dolor...")
}
}
.padding(.horizontal)
.navigationBarTitleDisplayMode(.inline)
}
.navigationViewStyle(.stack)
}
}
Primary View
struct ListView: View {
#State private var selectedCard: Model?
#State private var isShowingCard: Bool = false
#Namespace var animation
var body: some View {
ZStack {
NavigationView {
ScrollView(.vertical, showsIndicators: false) {
ForEach(mockItems) { item in
CardView(item: item)
.matchedGeometryEffect(id: item.id, in: animation)
.onTapGesture {
withAnimation {
selectedCard = item
isShowingCard = true
}
}
}
}
.navigationTitle("Test title")
}
.padding(.horizontal)
.navigationViewStyle(.stack)
// show detail view
if let selectedCard = selectedCard, isShowingCard {
DetailView(
isShowingDetail: $isShowingCard,
item: selectedCard,
animation: animation
)
}
}
}
}
Video examples
With NavigationView in DetailView
Without NavigationView in DetailView
Ignore the list view still visible
You don't need second NavigationView (actually I don't see links at all, so necessity of the first one is also under question). Anyway we can just change the layout order and put everything into one NavigationView, like below.
Tested with Xcode 13.4 / iOS 15.5
struct ListView: View {
#State private var selectedCard: Model?
#State private var isShowingCard: Bool = false
#Namespace var animation
var body: some View {
NavigationView {
ZStack { // container !!
if let selectedCard = selectedCard, isShowingCard {
DetailView(
isShowingDetail: $isShowingCard,
item: selectedCard,
animation: animation
)
} else {
ScrollView(.vertical, showsIndicators: false) {
ForEach(mockItems) { item in
CardView(item: item)
.matchedGeometryEffect(id: item.id, in: animation)
.onTapGesture {
selectedCard = item
isShowingCard = true
}
}
}
.navigationTitle("Test title")
}
}
.padding(.horizontal)
.navigationViewStyle(.stack)
.animation(.default, value: isShowingCard) // << animated here !!
}
}
}
struct DetailView: View {
#Binding var isShowingDetail: Bool
let item: Model
let animation: Namespace.ID
var body: some View {
VStack {
CardView(item: item)
.matchedGeometryEffect(id: item.id, in: animation)
.onTapGesture {
isShowingDetail = false
}
ScrollView(.vertical, showsIndicators: false) {
Text("Lorem ipsum dolor...")
}
}
.padding(.horizontal)
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(.stack)
}
}
Test module is here

SwiftUI TabView is not working properly on Orientation Change

I created a simple tabView like this
struct ContentView: View {
var body: some View {
TabView{
TabItemView(color: .red, title: "Page 1")
TabItemView(color: .yellow, title: "Page 2")
TabItemView(color: .green, title: "Page 3")
TabItemView(color: .blue, title: "Page 4")
}
.tabViewStyle(.page)
.background(Color.indigo)
}
}
where
struct TabItemView: View {
var color : Color
var title : String
var body: some View {
Text("\(title)")
.frame(width:UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.background(color)
}
}
issue is when is switch to landscape entire thing is broken , like this
ie the tabs are jumped back to either some random position or position between two.
I searched online and some resources say its a bug thats not solved yet . Any solution for this?
Hi the bug is well known. Generally it helps when you put the tabview into inactive scrollview with certain frame modifier, like this:
struct ContentView: View {
var body: some View {
ScrollView([])
TabView{
TabItemView(color: .red, title: "Page 1")
TabItemView(color: .yellow, title: "Page 2")
TabItemView(color: .green, title: "Page 3")
TabItemView(color: .blue, title: "Page 4")
}
.tabViewStyle(.page)
.background(Color.indigo)
}
}
.frame(width: UIScreen.main.bounds.width)
}
This should fix the views in the middle in case of rotations. However, it might happen that when you use some array and iterate over it using ForEach inside the TabView, the elements are arbitrarily changed by TabView while rotating view. In that case it helps to keep tracking the current and previous orientation using states and build some logic onChange to prevent TabView to doing that. Like some wrapper adding layer between the binded state. Like:
struct TabView: View {
#State var pages: Int = 1
var array: [Int] = [1,2,3,4,5]
var body: some View {
VStack {
TabViewContent(selection: $pages, usage: .one) {
ForEach(array, id: \.self) { index in
Text("This is: \(pages) \(index)")
.tag(index)
}
}
}
}
}
Wrapper with logic:
struct TabViewContent<Selection: Hashable, Content: View>: View {
#Binding var selection: Selection
#State var selectionInternal: Selection
#State var previousOrientation: UIDeviceOrientation = .unknown
#State var currentOrientation: UIDeviceOrientation = .unknown
#ViewBuilder var content: () -> Content
internal init(selection: Binding<Selection>, content: #escaping () -> Content) {
self.content = content
_selection = selection
_selectionInternal = State(initialValue: selection.wrappedValue)
}
var body: some View {
TabView(selection: $selection, content: content)
.tabViewStyle(.page)
.onChange(of: UIDevice.current.orientation) { newOrientation in
currentOrientation = newOrientation
}
.onChange(of: selectionInternal) { value in
if currentOrientation == previousOrientation {
selection = selectionInternal
}
previousOrientation = currentOrientation
}
}
}

How can add custom fonts and images to my picker?

I have a picker in my app that displays the currently picked option as its label. However, I can't seem to figure out how to change the font used by the picker for its label and for displaying all the picker options. I would also like to put an icon in front of the text of the picker that's part of the picker button but doesn't change as the text does so that a user could click both the text and the icon for the picker to appear.
Here's the code I am using for my picker:
var fylker = ["Norge", "Akershus", "Agder", "Buskerud", "Finnmark", "Innlandet", "Møre og Romsdal", "Nordland", "Oslo", "Østfold", "Rogaland", "Troms", "Trøndelag", "Vestfold og Telemark", "Vestland"]
Picker("Fylke", selection: $selectedFylke) {
ForEach(fylker, id: \.self) {
Text($0)
}
}
I've already tried things like:
Picker("Fylke", selection: $selectedFylke) {
ForEach(fylker, id: \.self) {
Text($0).font(.custom("Custom-Bold", size: 34))
}
}
and
Picker("Fylke", selection: $selectedFylke) {
ForEach(fylker, id: \.self) {
HStack {
Image(systemName: "chevron.forward.circle")
Text($0)
}
}
}
to no avail
Any ideas?
Thanks!
from what i understand i think you could try to use a font modifier to add your custom font and add image inside HStack so they are beside each other like that
import SwiftUI
struct SwiftUIView: View {
var fylker = ["Norge", "Akershus", "Agder", "Buskerud", "Finnmark",
"Innlandet", "Møre og Romsdal", "Nordland", "Oslo", "Østfold",
"Rogaland", "Troms", "Trøndelag", "Vestfold og Telemark", "Vestland"]
#State var selectedFylke:String = ""
var body: some View {
Picker("Fylke", selection: $selectedFylke) {
ForEach(fylker, id: \.self) { I in
HStack(){
Text(I).font(Font.custom("Font-Family-Name", size: 16))
Image(systemName: "pencil")
}.tag(I)
}
}.frame(width: 200, height: 60, alignment:.center).background(Color.red)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
also if you mean to add the image beside the picker so they seemed as a button picker you can do that
import SwiftUI
struct SwiftUIView: View {
var fylker = ["Norge", "Akershus", "Agder", "Buskerud", "Finnmark",
"Innlandet", "Møre og Romsdal", "Nordland", "Oslo", "Østfold",
"Rogaland", "Troms", "Trøndelag", "Vestfold og Telemark", "Vestland"]
#State var selectedFylke:String = ""
var body: some View {
HStack(){
Image(systemName: "pencil").foregroundColor(.white)
Picker("search", selection: $selectedFylke) {
ForEach(fylker, id: \.self) {
Text($0).font(Font.custom("Font-Family-Name", size: 16))
}
}
}.frame(width: 200, height: 60, alignment:
.center).background(Color.red).background(Color.red).cornerRadius(30)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
try this approach to set the font used by the Picker:
struct ContentView: View {
let fylker = ["Norge", "Akershus", "Agder", "Buskerud", "Finnmark",
"Innlandet", "Møre og Romsdal", "Nordland", "Oslo", "Østfold",
"Rogaland", "Troms", "Trøndelag", "Vestfold og Telemark", "Vestland"]
#State var selectedFylke = ""
var body: some View {
Picker("Fylke", selection: $selectedFylke) {
ForEach(fylker, id: \.self) { item in
HStack {
Image(systemName: "chevron.forward.circle")
Text(item)
}
.font(.custom("Custom-Bold", size: 34)) // for both the Text and the Image
.tag(item)
}
}.pickerStyle(.wheel) // <-- here
}
}
This does not work with pickerStyle: automatic, menu and segmented

SwiftUI setting individual backgrounds for list Elements that fill the element

So I'm trying to make a list where the background of each element is an image that completely fills the element and I'm struggling to get it to completely fill the area.
here's some simple example code showing what I have so far.
struct Event {
var description:String = "description"
var title:String = "Title"
var view:Image
}
struct RowView: View {
#State var event:Event
var body: some View {
VStack {
Text(event.title)
.font(.headline)
Text(event.description)
.font(.subheadline)
}
}
}
struct ContentView: View {
#State var events:[Event] = [Event(view: Image("blue")),Event( view: Image("red")),Event( view: Image("green"))]
#State private var editMode = EditMode.inactive
var body: some View {
NavigationView {
List() {
ForEach(events.indices, id: \.self) { elementID in
NavigationLink(
destination: RowView(event: events[elementID])) {
RowView(event: events[elementID])
}
.background(events[elementID].view)
.clipped()
}
}
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
.navigationTitle("Events")
.navigationBarItems(leading: EditButton())
.environment(\.editMode, $editMode)
}
}
}
This is what's produced by the code as you can see there is a border around the background image still.
I don't know what your images are, but seems that should not be important. Here is a demo on replicated code using colors.
Tested with Xcode 12.1 / iOS 14.1
Modified parts (important highlighted in comments)
struct Event {
var description:String = "description"
var title:String = "Title"
var view:Color
}
struct ContentView: View {
#State var events:[Event] = [Event(view: .blue),Event( view: .red),Event( view: .green)]
#State private var editMode = EditMode.inactive
var body: some View {
NavigationView {
List() {
ForEach(events.indices, id: \.self) { elementID in
events[elementID].view.overlay( // << this !!
NavigationLink(
destination: RowView(event: events[elementID])) {
RowView(event: events[elementID])
})
}
.listRowInsets(EdgeInsets()) // << this !!
}
.listStyle(PlainListStyle()) // << and this !!
.navigationTitle("Events")
.navigationBarItems(leading: EditButton())
.environment(\.editMode, $editMode)
}
}
}
This works:
struct RowView: View {
#State var event:Event
var body: some View {
VStack(alignment: .leading) {
Text(event.title)
.font(.headline)
Text(event.description)
.font(.subheadline)
}
.padding(10)
.foregroundColor(.yellow)
}
}
struct ContentView: View {
#State var events:[Event] = [Event(view: Image("blue")),Event( view: Image("red")),Event( view: Image("green"))]
#State private var editMode = EditMode.inactive
var body: some View {
NavigationView {
List() {
ForEach(events.indices, id: \.self) { elementID in
NavigationLink(
destination: RowView(event: events[elementID])) {
RowView(event: events[elementID])
}
.background(events[elementID].view
.resizable()
.aspectRatio(contentMode: .fill)
)
.clipped()
}
.listRowInsets(EdgeInsets())
}
.listStyle(PlainListStyle())
.navigationTitle("Events")
.navigationBarItems(leading: EditButton())
.environment(\.editMode, $editMode)
}
}
}
This is a derivative of Asperi's answer, so you can accept his answer, but this works with images, and no overlay. Tested on Xcode 12.1, IOS 14.1.
Here is an image where I grabbed arbitrary sized images for "red", "green", and "blue" from Goggle. I took the liberty of changing the row text to yellow so it would show up better.

SwiftUI Programmatically Select List Item

I have a SwiftUI app with a basic List/Detail structure. A new item is created from
a modal sheet. When I create a new item and save it I want THAT list item to be
selected. As it is, if no item is selected before an add, no item is selected after
an add. If an item is selected before an add, that same item is selected after the
add.
I'll include code for the ContentView, but this is really the simplest example of
List/Detail.
struct ContentView: View {
#ObservedObject var resortStore = ResortStore()
#State private var addNewResort = false
#State private var coverDeletedDetail = false
#Environment(\.presentationMode) var presentationMode
var body: some View {
List {
ForEach(resortStore.resorts) { resort in
NavigationLink(destination: ResortView(resort: resort)) {
HStack(spacing: 20) {
Image("FlatheadLake1")
//bunch of modifiers
VStack(alignment: .leading, spacing: 10) {
//the cell contents
}
}
}
}
.onDelete { indexSet in
self.removeItems(at: [indexSet.first!])
self.coverDeletedDetail.toggle()
}
if UIDevice.current.userInterfaceIdiom == .pad {
NavigationLink(destination: WelcomeView(), isActive: self.$coverDeletedDetail) {
Text("")
}
}
}//list
.onAppear(perform: self.selectARow)
.navigationBarTitle("Resorts")
.navigationBarItems(leading:
//buttons
}//body
func removeItems(at offsets: IndexSet) {
resortStore.resorts.remove(atOffsets: offsets)
}
func selectARow() {
//nothing that I have tried works here
print("selectARow")
}
}//struct
And again - the add item modal is extremely basic:
struct AddNewResort: View {
//bunch of properties
var body: some View {
VStack {
Text("Add a Resort")
VStack {
TextField("Enter a name", text: $resortName)
//the rest of the fields
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(EdgeInsets(top: 20, leading: 30, bottom: 20, trailing: 30))
Button(action: {
let newResort = Resort(id: UUID(), name: self.resortName, country: self.resortCountry, description: self.resortDescription, imageCredit: "Credit", price: Int(self.resortPriceString) ?? 0, size: Int(self.resortSizeString) ?? 0, snowDepth: 20, elevation: 3000, runs: 40, facilities: ["bar", "garage"])
self.resortStore.resorts.append(newResort)
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Save Trip")
}
.padding(.trailing, 20)
}
}
}
To show the issue - The list with a selection:
The list after a new item created showing the previous selection:
Any guidance would be appreciated. Xcode 11.4
I tried to reconstitute your code as closely as could so that it builds. Here is what I have in the end. We have a list of resorts and when a new resort is saved in the AddNewResort sheet, if we are currently in split view (horizontalSizeClass is regular), we will select the new resort, otherwise just dismiss the sheet.
import SwiftUI
class ResortStore: ObservableObject {
#Published var resorts = [Resort(id: UUID(), name: "Resort 1")]
}
struct ContentView: View {
#ObservedObject var resortStore = ResortStore()
#State private var addingNewResort = false
#State var selectedResortId: UUID? = nil
var navigationLink: NavigationLink<EmptyView, ResortView>? {
guard let selectedResortId = selectedResortId,
let selectedResort = resortStore.resorts.first(where: {$0.id == selectedResortId}) else {
return nil
}
return NavigationLink(
destination: ResortView(resort: selectedResort),
tag: selectedResortId,
selection: $selectedResortId
) {
EmptyView()
}
}
var body: some View {
NavigationView {
ZStack {
navigationLink
List {
ForEach(resortStore.resorts, id: \.self.id) { resort in
Button(action: {
self.selectedResortId = resort.id
}) {
Text(resort.name)
}
.listRowBackground(self.selectedResortId == resort.id ? Color.gray : Color(UIColor.systemBackground))
}
}
}
.navigationBarTitle("Resorts")
.navigationBarItems(trailing: Button("Add Resort") {
self.addingNewResort = true
})
.sheet(isPresented: $addingNewResort) {
AddNewResort(selectedResortId: self.$selectedResortId)
.environmentObject(self.resortStore)
}
WelcomeView()
}
}
}
struct ResortView: View {
let resort: Resort
var body: some View {
Text("Resort View for resort name: \(resort.name).")
}
}
struct AddNewResort: View {
//bunch of properties
#Binding var selectedResortId: UUID?
#State var resortName = ""
#Environment(\.presentationMode) var presentationMode
#Environment(\.horizontalSizeClass) var horizontalSizeClass
#EnvironmentObject var resortStore: ResortStore
var body: some View {
VStack {
Text("Add a Resort")
VStack {
TextField("Enter a name", text: $resortName)
//the rest of the fields
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(EdgeInsets(top: 20, leading: 30, bottom: 20, trailing: 30))
Button(action: {
let newResort = Resort(id: UUID(), name: self.resortName)
self.resortStore.resorts.append(newResort)
self.presentationMode.wrappedValue.dismiss()
if self.horizontalSizeClass == .regular {
self.selectedResortId = newResort.id
}
}) {
Text("Save Trip")
}
.padding(.trailing, 20)
}
}
}
struct WelcomeView: View {
var body: some View {
Text("Welcome View")
}
}
struct Resort {
var id: UUID
var name: String
}
We need to keep track of the selectedResortId
We create an invisible NavigationLink that will programmatically navigate to the selected resort
We make our list row a Button, so that the user can select a resort by tapping on the row
I started writing a series of articles about navigation in SwiftUI List view, there are a lot of points to consider while implementing programmatic navigation.
Here is the one that describes this solution that I'm suggesting: SwiftUI Navigation in List View: Programmatic Navigation. This solution works at the moment on iOS 13.4.1. SwiftUI is changing rapidly, so we have to keep on checking.
And here is my previous article that explains why a more simple solution of adding a NavigationLink to each List row has some problems at the moment SwiftUI Navigation in List View: Exploring Available Options
Let me know if you have questions, I'd be happy to help where I can.

Resources