How to Change Item's Order in a List View? - swift

I want to allow users to change the order of photos in the list. Tinder has something like that. I'm trying to add a drag gesture to the item but nothing happened.
import SwiftUI
struct Row: Identifiable {
let id = UUID()
let cells: [Cell]
}
struct Cell: Identifiable {
let id = UUID()
let imageURL: String
#State var offset = CGSize.zero
}
struct PhotosView: View {
#State var rows = [Row(cells: [Cell(imageURL: "gigi"), Cell(imageURL: "hadid"), Cell(imageURL: "gigihadid")]), Row(cells: [Cell(imageURL: "gigi"), Cell(imageURL: "hadid")])]
var body: some View {
List {
ForEach(rows) { row in
HStack(alignment: .center, spacing:15) {
ForEach(row.cells) { cell in
Image(cell.imageURL)
.resizable()
.scaledToFill()
.frame(maxWidth: UIScreen.main.bounds.width / 3.6)
.clipped()
.cornerRadius(10)
.offset(x: cell.offset.width, y: cell.offset.height)
.gesture(
DragGesture()
.onChanged { gesture in
cell.offset = gesture.translation
}
.onEnded { _ in
if abs(cell.offset.width) > 100 {
// remove the card
} else {
cell.offset = .zero
}
}
)
}
if row.cells.count < 3 {
ZStack{
RoundedRectangle(cornerRadius: 10)
.fill(Color(UIColor.systemGray6))
Button(action: {}, label: {
Image(systemName: "person.crop.circle.fill.badge.plus")
.font(.title)
.foregroundColor(Color(UIColor.systemGray3))
})
}
}
}.padding(.vertical)
}.buttonStyle(PlainButtonStyle())
}.padding(.top, UIScreen.main.bounds.height / 8)
.navigationBarHidden(true)
}
}
What view looks like:
I only want users can change the order of photos via drag gesture.

Related

Swift/SwiftUI: Using AppStorage to hold list of IDs

I am trying to use AppStorage to hold a list of id's for my Country struct. I want to persist this list of id's as favorites for a user and if it's a favorite, have the heart next to each country filled.
If they tap the heart to unfavorite it, it will remove the id from the favLists and if they tap it to favorite it, it will add it to the list.
This is not working as I expect and I am not sure what I am doing wrong here.
struct Number: Identifiable, Codable {
var id: Int
}
struct ContentView: View {
#Binding var countries: [Country]
#Namespace var namespace;
#AppStorage("favLists") var favLists: [Number] = [];
var body: some View {
GeometryReader { bounds in
NavigationView {
ScrollView {
ForEach(Array(filteredCountries.enumerated()), id: \.1.id) { (index,country) in
LazyVStack {
ZStack(alignment: .bottom) {
HStack {
NavigationLink(
destination: CountryView(country: country),
label: {
HStack {
Image(country.image)
.resizable()
.frame(width: 50, height: 50)
Text(country.display_name)
.foregroundColor(Color.black)
.padding(.leading)
Spacer()
}
.padding(.top, 12.0)
}
).buttonStyle(FlatLinkStyle())
if (favLists.filter{$0.id == country.id}.count > 0) {
Image(systemName: "heart.fill").foregroundColor(.red).onTapGesture {
let index = favLists.firstIndex{ $0.id == country.id}
if let index = index {
favLists.remove(at: index)
}
}
.padding(.top, 12)
} else {
Image(systemName: "heart").foregroundColor(.red).onTapGesture {
favLists.append(Number(id: country.id))
}
.padding(.top, 12)
}
}
.padding(.horizontal, 16.0)
}
}
}
}
.frame(maxWidth: bounds.size.width)
.navigationTitle("Countries")
.font(Font.custom("Avenir-Book", size: 28))
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}

Trying to create a grid of numbers that must be clicked in order and when clicked the background changes color SWIFTUI

As the title says I am trying to create a grid of random numbers that must be clicked in ascending order and when pressed the background changes color so they're no longer visible.
I figured out how to create the grid from 0 to what ever size grid I want (5x5, 10x10, etc...)
I want the background to be white to start and then when the button is pressed it changes to black
The two biggest issues I'm having are all of the buttons turning black after I press on one button and the numbers randomize every time I press a button.
Any one have any ideas?
import SwiftUI
struct ContentView: View {
#State var buttonbackg = Color.white
#State var gridsize = 100
var body: some View {
//make grid
let cellCount = (gridsize/10)
let r = numbrand(min: 00, max: ((gridsize/10) * (gridsize/10) - 1))
ZStack{
Rectangle()
.ignoresSafeArea()
.background(Color.black)
VStack{
Text("Concentration Grid")
.font(.title)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.padding(.bottom)
Spacer()
}
VStack(spacing:3) {
Spacer()
ForEach(1...cellCount, id:\.self) { i in
HStack(spacing:3) {
Spacer()
ForEach(1...cellCount, id:\.self) { c in
let a = r()
ZStack {
Button(action:{
if (self.buttonbackg == .white) {
self.buttonbackg = .black
}
}){
Text("\(a)")
.foregroundColor(Color.black)
.frame(width: 28, height: 28)
.background(buttonbackg)
.cornerRadius(5)
.multilineTextAlignment(.center)
.scaledToFit()
.padding(.all, -2)
Spacer()
}
Spacer()
}
}
}
Spacer()
}
Spacer()
}
}
}
//^grid
func numbrand(min: Int, max:Int) -> () -> Int{
//let array: [Int] = Array(min...max)
var numbers: [Int] = []
return {
if numbers.isEmpty {
numbers = Array(min...max)
}
let index = Int(arc4random_uniform(UInt32(numbers.count)))
return numbers.remove(at: index)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Every-time you click a button, view redraws itself and thus your randomArray is created again
You need to create a Struct here which holds the property whether the
button is selected or not.
Give it a try this way:
struct concentrationGame: Hashable {
var id = UUID()
var number: Int = 0
var isSelected: Bool = false
}
class RandomNumbers: ObservableObject {
#Published var randomNumbers : [concentrationGame]
init() {
self.randomNumbers = (1...100).map{_ in arc4random_uniform(100)}.map({ concentrationGame(number: Int($0)) })
}
}
struct ContentView: View {
#ObservedObject var randomNumbers = RandomNumbers()
private var gridItemLayout = [GridItem(.adaptive(minimum: 50))]
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: gridItemLayout, spacing: 20) {
ForEach($randomNumbers.randomNumbers, id: \.self) { $randomNumbers in
Button(String(randomNumbers.number)) { randomNumbers.isSelected.toggle() }
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 50)
.foregroundColor(.white)
.background( randomNumbers.isSelected ? .white : .black)
.cornerRadius(10)
}
}
}
.padding()
.ignoresSafeArea(edges: .bottom)
.navigationTitle("Concentration Grid")
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

TabView Indicator doesn't move when page changes

When I scroll from one page to the other the horizontal indicator bar doesn't move, what would be the appropriate way to move it (with animation if possible)?
The green area below doesn't move to the right once I switch to the analytics area.
Here's the full code:
enum PortfolioTabBarOptions: Hashable, CaseIterable {
case balance, analytics
var menuString: String { String(describing: self) }
}
struct CustomPagerTabView: View {
#Binding var selectedTab: PortfolioTabBarOptions
#State var tabOffset: CGFloat = 0
var body: some View {
VStack {
HStack(alignment: .center) {
HStack(spacing: 100) {
ForEach(Array(PortfolioTabBarOptions.allCases.enumerated()), id: \.element) { index, element in
// Current Tab
if(selectedTab.menuString == element.menuString) {
Text(element.menuString.capitalizeFirstLetter())
.font(.system(size: 15.0))
.bold()
.onTapGesture() {
selectedTab = element.self
}
.onAppear {
self.tabOffset = CGFloat(index)
}
}
// Innactive Tabs
else {
Text(element.menuString.capitalizeFirstLetter())
.foregroundColor(.gray)
.font(.system(size: 15.0))
.bold()
.onTapGesture() {
selectedTab = element.self
}
}
}
}
.padding(.horizontal)
}
// Indicator...
Capsule()
.fill(.gray)
// Width of the indicator bar
.frame(width: PortfolioTabBarOptions.allCases.count == 0 ? 0 : (getScreenBounds().width / CGFloat(PortfolioTabBarOptions.allCases.count)), height: 1.2)
.padding(.top,10)
.frame(maxWidth: .infinity,alignment: .leading)
.offset(x: tabOffset)
}
.padding(.top)
}
}
The green area below does move to the right, but only 1 pixel. Try something like this example code, choose the value (200) most suited for your purpose:
.onAppear {
self.tabOffset = CGFloat(index*200) // <-- here
}

How to swipe to delete in SwiftUI with only a ForEach and NOT a List

I am making a custom list using a ForEach in SwiftUI. My goal is to make a swipe to delete gesture and not embed the ForEach into a List.
This is my code so far:
import SwiftUI
struct ContentView: View {
let list = ["item1", "item2", "item3", "item4", "item5", "item6"]
var body: some View {
VStack {
List{
ForEach(list, id: \.self) { item in
Text(item)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
.background(Color.red)
.cornerRadius(20)
.padding()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I can't seem to find a gesture that will allow me to make a swipe to delete without using the list view.
I would also like to make a custom delete button what is shown when the user swipes an item to the left (like the below picture).
This is my solution for your problem. You should add DragGesture and create offset for each row.
Keep in mind that your variable declare by var can't be mutated. You have to add #State before.
struct ContentView: View {
#State var list = ["item1", "item2", "item3", "item4", "item5", "item6"]
#State private var offsets = [CGSize](repeating: CGSize.zero, count: 6)
var body: some View {
VStack {
ForEach(list.indices, id: \.self) { index in
HStack {
Text(list[index])
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
.background(Color.red)
.cornerRadius(20)
.padding()
Button(action: {
self.list.remove(at: index)
self.offsets.remove(at: index)
}) {
Image(systemName: "xmark")
}
}
.padding(.trailing, -40)
.offset(x: offsets[index].width)
.gesture(
DragGesture()
.onChanged { gesture in
self.offsets[index] = gesture.translation
if offsets[index].width > 50 {
self.offsets[index] = .zero
}
}
.onEnded { _ in
if self.offsets[index].width < -100 {
self.list.remove(at: index)
self.offsets.remove(at: index)
}
else if self.offsets[index].width < -50 {
self.offsets[index].width = -50
}
}
)
}
}
}
}

SwiftUI create image slider with dots as indicators

I want to create a scroll view/slider for images. See my example code:
ScrollView(.horizontal, showsIndicators: true) {
HStack {
Image(shelter.background)
.resizable()
.frame(width: UIScreen.main.bounds.width, height: 300)
Image("pacific")
.resizable()
.frame(width: UIScreen.main.bounds.width, height: 300)
}
}
Though this enables the user to slide, I want it a little different (similar to a PageViewController in UIKit). I want it to behave like the typical image slider we know from a lot of apps with dots as indicators:
It shall always show a full image, no in between - hence if the user drags and stops in the middle, it shall automatically jump to the full image.
I want dots as indicators.
Since I've seen a lot of apps use such a slider, there must be known method, right?
There is no built-in method for this in SwiftUI this year. I'm sure a system-standard implementation will come along in the future.
In the short term, you have two options. As Asperi noted, Apple's own tutorials have a section on wrapping the PageViewController from UIKit for use in SwiftUI (see Interfacing with UIKit).
The second option is to roll your own. It's entirely possible to make something similar in SwiftUI. Here's a proof of concept, where the index can be changed by swipe or by binding:
struct PagingView<Content>: View where Content: View {
#Binding var index: Int
let maxIndex: Int
let content: () -> Content
#State private var offset = CGFloat.zero
#State private var dragging = false
init(index: Binding<Int>, maxIndex: Int, #ViewBuilder content: #escaping () -> Content) {
self._index = index
self.maxIndex = maxIndex
self.content = content
}
var body: some View {
ZStack(alignment: .bottomTrailing) {
GeometryReader { geometry in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
self.content()
.frame(width: geometry.size.width, height: geometry.size.height)
.clipped()
}
}
.content.offset(x: self.offset(in: geometry), y: 0)
.frame(width: geometry.size.width, alignment: .leading)
.gesture(
DragGesture().onChanged { value in
self.dragging = true
self.offset = -CGFloat(self.index) * geometry.size.width + value.translation.width
}
.onEnded { value in
let predictedEndOffset = -CGFloat(self.index) * geometry.size.width + value.predictedEndTranslation.width
let predictedIndex = Int(round(predictedEndOffset / -geometry.size.width))
self.index = self.clampedIndex(from: predictedIndex)
withAnimation(.easeOut) {
self.dragging = false
}
}
)
}
.clipped()
PageControl(index: $index, maxIndex: maxIndex)
}
}
func offset(in geometry: GeometryProxy) -> CGFloat {
if self.dragging {
return max(min(self.offset, 0), -CGFloat(self.maxIndex) * geometry.size.width)
} else {
return -CGFloat(self.index) * geometry.size.width
}
}
func clampedIndex(from predictedIndex: Int) -> Int {
let newIndex = min(max(predictedIndex, self.index - 1), self.index + 1)
guard newIndex >= 0 else { return 0 }
guard newIndex <= maxIndex else { return maxIndex }
return newIndex
}
}
struct PageControl: View {
#Binding var index: Int
let maxIndex: Int
var body: some View {
HStack(spacing: 8) {
ForEach(0...maxIndex, id: \.self) { index in
Circle()
.fill(index == self.index ? Color.white : Color.gray)
.frame(width: 8, height: 8)
}
}
.padding(15)
}
}
and a demo
struct ContentView: View {
#State var index = 0
var images = ["10-12", "10-13", "10-14", "10-15"]
var body: some View {
VStack(spacing: 20) {
PagingView(index: $index.animation(), maxIndex: images.count - 1) {
ForEach(self.images, id: \.self) { imageName in
Image(imageName)
.resizable()
.scaledToFill()
}
}
.aspectRatio(4/3, contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 15))
PagingView(index: $index.animation(), maxIndex: images.count - 1) {
ForEach(self.images, id: \.self) { imageName in
Image(imageName)
.resizable()
.scaledToFill()
}
}
.aspectRatio(3/4, contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 15))
Stepper("Index: \(index)", value: $index.animation(.easeInOut), in: 0...images.count-1)
.font(Font.body.monospacedDigit())
}
.padding()
}
}
Two notes:
The GIF animation does a really poor job of showing how smooth the animation is, as I had to drop the framerate and compress heavily due to file size limits. It looks great on simulator or a real device
The drag gesture in the simulator feels clunky, but it works really well on a physical device.
You can easily achieve this by below code
struct ContentView: View {
public let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
#State private var selection = 0
/// images with these names are placed in my assets
let images = ["1","2","3","4","5"]
var body: some View {
ZStack{
Color.black
TabView(selection : $selection){
ForEach(0..<5){ i in
Image("\(images[i])")
.resizable()
.aspectRatio(contentMode: .fit)
}
}.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
.onReceive(timer, perform: { _ in
withAnimation{
print("selection is",selection)
selection = selection < 5 ? selection + 1 : 0
}
})
}
}
}