SwiftUI Drag And Drop with List - list

I try to implement drag and drop between two views as shown in the code bellow, when I embed the "ForEach" view inside Group its work well, but when I try to embed it in the "List" view it doesn't work and I don't know why, any one can help to solve this problem ??
import SwiftUI
import MobileCoreServices
struct DragAndDropExample: View {
var delegate = dropDelegate()
#State var data = [SomeData(id: 1), SomeData(id: 2), SomeData(id: 3), SomeData(id: 4), SomeData(id: 5)]
#State var selectedDate: [SomeData] = []
var body: some View {
VStack (spacing: 16) {
Group { // Try to maek this "List"
ForEach(data) { dat in
Image(systemName: "\(dat.data).square.fill")
.resizable()
.background(Color.blue)
.frame(width: 50, height: 50)
.cornerRadius(15)
.onDrag{
NSItemProvider(item: .some(URL(string: dat.data)! as NSSecureCoding), typeIdentifier: String(kUTTypeURL))
}
}
}
Group { // Try to maek this "List"
if selectedDate.isEmpty {
Text("Drop Here")
}else {
ForEach(selectedDate) { dat in
Text("\(dat.data)")
}
}
}
.background(Color.blue)
.onDrop(of: [String(kUTTypeURL)], delegate: delegate)
}
}
}
struct DragAndDropExample_Previews: PreviewProvider {
static var previews: some View {
DragAndDropExample()
}
}
class SomeData: Identifiable {
let id: Int
let data: String
init(id: Int) {
self.id = id
self.data = String(id)
}
}
class dropDelegate: DropDelegate {
func performDrop(info: DropInfo) -> Bool {
// Drag and drop code here...
return true
}
}

Related

SwiftUI's custom modal logic: how to present view from parent

Coming from UIKit, I'm building my own modal navigation logic in SwiftUI, because I want custom layouts and animations. Here, I want a generic bottom sheet like so:
I have achieved something close with the following code:
enum ModalType: Equatable {
case normal // ...
#ViewBuilder
var screen: some View {
switch self {
case .normal: ModalView()
// ...
}
}
}
struct ContentView: View {
#State var presentedModal: ModalType?
var body: some View {
VStack {
Button("Present modal", action: { presentedModal = .normal }).foregroundColor(.black)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.gray)
.modifier(ModalBottomViewModifier(item: $presentedModal) { $0.screen })
}
}
struct ModalView: View {
#Environment(\.dismissModal) private var dismissModal
var body: some View {
VStack {
Button("Close", action: { dismissModal() })
}
.frame(maxWidth: .infinity)
.frame(height: 300)
.background(
RoundedRectangle(cornerRadius: 32)
.fill(.black.opacity(0.5))
.edgesIgnoringSafeArea([.bottom])
)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
// MARK: - Modal logic
struct ModalBottomViewModifier<Item:Equatable, V:View>: ViewModifier {
#Binding var item: Item?
#ViewBuilder var view: (Item) -> V
func body(content: Content) -> some View {
ZStack(alignment: .bottom) {
content
if let item = item {
view(item)
.environment(\.dismissModal, { self.item = nil })
.transition(.move(edge: .bottom))
}
}
.animation(.easeOut, value: item)
}
}
private struct ModalDismissKey: EnvironmentKey {
static let defaultValue: () -> Void = {}
}
extension EnvironmentValues {
var dismissModal: () -> Void {
get { self[ModalDismissKey.self] }
set { self[ModalDismissKey.self] = newValue }
}
}
Now I'd like to make this system reusable, so that I don't have to add the ModalBottomViewModifier to all my app screens. For that, I'd like to be able to apply the modifier to the button instead of the screen, just like it's possible with fullScreenCover:
Button("Present modal", action: { isPresented = true }).foregroundColor(.black)
.fullScreenCover(isPresented: $isPresented) { ModalView() }
This is not possible with my current solution, because the modal view will appear next to the button and not fullscreen.
How can I achieve this? Or should I be doing something different?
Here's a simple solution using UIKit:
extension View {
func presentModalView<Content: View, Item: Equatable>(item: Binding<Item?>, #ViewBuilder view: #escaping (Item) -> Content) -> some View {
func present() {
guard let itemy = item.wrappedValue else {return}
DispatchQueue.main.async {
let topMostController = self.topMostController()
let someView = VStack {
Spacer()
view(itemy)
.environment(\.dismissModal, {item.wrappedValue = nil})
}
let viewController = UIHostingController(rootView: someView)
viewController.view?.backgroundColor = .clear
viewController.modalPresentationStyle = .overFullScreen
topMostController.present(viewController, animated: true)
}
}
return self.onChange(of: item.wrappedValue) { value in
if value != nil {
present()
}else {
topMostController().dismiss(animated: true)
}
}.onAppear {
if item.wrappedValue != nil {
present()
}
}
}
func topMostController() -> UIViewController {
var topController: UIViewController = UIApplication.shared.windows.first!.rootViewController!
while (topController.presentedViewController != nil) {
topController = topController.presentedViewController!
}
return topController
}
}
Usage:
struct ContentView: View {
#State var presentedModal: ModalType?
var body: some View {
VStack {
Button("Present modal", action: { presentedModal = .normal }).foregroundColor(.black)
.presentModalView(item: $presentedModal, view: {$0.screen})
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.gray)
}
}

How to set up Textfield and Button in Custom SwiftUI View

I am attempting the configure the text field and button in my openweathermap app to be in its own view other than the main content view. In TextFieldView, the action of the button is set up to call an API response. Then, the weather data from the response is populated on a sheet-based DetailView, which is triggered by the button in TextFieldView. I configured the ForEach method in the sheet to return the last city added to the WeatherModel array (which would technically be the most recent city entered into the text field), then populate the sheet-based DetailView with weather data for that city. Previously, When I had the HStack containing the text field, button, and sheet control set up in the ContentView, the Sheet would properly display weather for the city that had just entered into the text field. After moving those items to a separate TextFieldView, the ForEach method appears to have stopped working. Instead, the weather info returned after entering a city name into the text field is displayed on the wrong count. For instance, if I were to enter "London" in the text field, the DetailView in the sheet is completely blank. If I then enter "Rome" as the next entry, the DetailView in the sheet shows weather info for the previous "London" entry. Entering "Paris" in the textfield displays weather info for "Rome", and so on...
To summarize, the ForEach method in the sheet stopped working properly after I moved the whole textfield and button feature to a separate view. Any idea why the issue I described is happening?
Here is my code:
ContentView
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
var body: some View {
NavigationView {
VStack(alignment: .leading) {
List {
ForEach(viewModel.cityNameList.reversed()) { city in
NavigationLink(destination: DetailView(detail: city), label: {
Text(city.name).font(.system(size: 18))
Spacer()
Text("\(city.main.temp, specifier: "%.0f")°")
.font(.system(size: 18))
})
}
.onDelete { indexSet in
let reversed = Array(viewModel.cityNameList.reversed())
let items = Set(indexSet.map { reversed[$0].id })
viewModel.cityNameList.removeAll { items.contains($0.id) }
}
}
.refreshable {
viewModel.updatedAll()
}
TextFieldView(viewModel: viewModel)
}.navigationBarTitle("Weather", displayMode: .inline)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
TextFieldView
struct TextFieldView: View {
#State private var cityName = ""
#State private var showingDetail = false
#FocusState var isInputActive: Bool
var viewModel: WeatherViewModel
var body: some View {
HStack {
TextField("Enter City Name", text: $cityName)
.focused($isInputActive)
Spacer()
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("Done") {
isInputActive = false
}
}
}
if isInputActive == false {
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
self.showingDetail.toggle()
}) {
Image(systemName: "plus")
.font(.largeTitle)
.frame(width: 75, height: 75)
.foregroundColor(Color.white)
.background(Color(.systemBlue))
.clipShape(Circle())
}
.sheet(isPresented: $showingDetail) {
ForEach(0..<viewModel.cityNameList.count, id: \.self) { city in
if (city == viewModel.cityNameList.count-1) {
DetailView(detail: viewModel.cityNameList[city])
}
}
}
}
}
.frame(minWidth: 100, idealWidth: 150, maxWidth: 500, minHeight: 30, idealHeight: 40, maxHeight: 50, alignment: .leading)
.padding(.leading, 16)
.padding(.trailing, 16)
}
}
struct TextFieldView_Previews: PreviewProvider {
static var previews: some View {
TextFieldView(viewModel: WeatherViewModel())
}
}
DetailView
struct DetailView: View {
#State private var cityName = ""
#State var selection: Int? = nil
var detail: WeatherModel
var body: some View {
VStack(spacing: 20) {
Text(detail.name)
.font(.system(size: 32))
Text("\(detail.main.temp, specifier: "%.0f")°")
.font(.system(size: 44))
Text(detail.firstWeatherInfo())
.font(.system(size: 24))
}
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(detail: WeatherModel.init())
}
}
ViewModel
class WeatherViewModel: ObservableObject {
#Published var cityNameList = [WeatherModel]()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName.escaped())&units=imperial&appid=<YourAPIKey>") else { return }
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else { return }
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.addToList(model)
}
}
catch {
print(error)
}
}
task.resume()
}
func updatedAll() {
// keep a copy of all the cities names
let listOfNames = cityNameList.map{$0.name}
// fetch the up-to-date weather info
for city in listOfNames {
fetchWeather(for: city)
}
}
func addToList( _ city: WeatherModel) {
// if already have this city, just update
if let ndx = cityNameList.firstIndex(where: {$0.name == city.name}) {
cityNameList[ndx].main = city.main
cityNameList[ndx].weather = city.weather
} else {
// add a new city
cityNameList.append(city)
}
}
}
Model
struct WeatherModel: Identifiable, Codable {
let id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Double = 0.0
var humidity = 0
}
struct WeatherInfo: Codable {
var description: String = ""
}
You need to use an ObservedObject in your TextFieldView to use your
original (single source of truth) #StateObject var viewModel that you create in ContentView and observe any change to it.
So use this:
struct TextFieldView: View {
#ObservedObject var viewModel: WeatherViewModel
...
}

.onDrop not working in views within a Form and Section in SwiftUI

My drop delegate doesn't get called when attached to view that is inside Form/Section. See code below.
struct ContentView: View {
#State private var text = "Goodbye" {
didSet {
print(text)
}
}
var body: some View {
Form {
Section {
Text(text)
RoundedRectangle(cornerRadius: 10)
.frame(width: 150, height: 150)
.onDrop(of: ["text"], delegate: MyDropDelegate(text: $text))
}
}
}
}
struct MyDropDelegate: DropDelegate {
#Binding var text: String
func performDrop(info: DropInfo) -> Bool {
self.text = "Received drop text"
return true
}
}
Comment out the Form/Section and it works fine.
What is the problem?

How to update Navigation Link subscript with an array of views inside a ForEach Loop?

I am utilizing a search bar from a Kavsoft Tutorial here: "https://www.youtube.com/watch?v=nuag1PILxCA&t=14s", I'm wondering on how to add navigation links to each of the items, I decided on embedding the itemView inside a navigation link with an array of views to loop through but it seems that it doesn't accept the index as a parameter giving "Cannot convert value of type 'item' to expected argument type 'Int'", instead I incremented the subscript on appear in the navigation link, although that updates the variable, but it doesn't seem to work for the different views themselves only navigating to the first view.
I've linked all the code needed to reproduce the problem but due to my incredibly limited experience in reproducing the problem in as less code as possible, I am not able to do so. Below the main issue of concern is the block starting from the VStack. Starting the program can be done by just adding Search_Bar() to content view body.
struct Home: View {
let views : [AnyView] = [ AnyView(untitled_Skull()), AnyView(dogs()), AnyView(cats()) ]
#Binding var filteredItems : [item]
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
var i = 0
VStack(spacing: 15){
ForEach(filteredItems){index in
NavigationLink(destination: views[i]
) {
itemView(item: index)
}.onAppear() {
i = i + 1
}
}
}
.padding()
}
}
}
func add(value: Int) -> Int {
let value = value + 1
return value
}
struct itemView: View {
var item: item
#State var show = false
var body: some View {
HStack(spacing: 15){
VStack {
let colorArray: [Color] = [.yellowLichtenstien, .redHaring, .orangeBasquiat, .pinkWarhol]
HStack {
Text(item.name)
.foregroundColor(.white)
.bold()
.padding(.leading)
Spacer()
}
HStack {
Text(item.subText)
.bold()
.foregroundColor (.white)
.font(.subheadline)
.padding(.leading)
Circle()
.frame(width: 5, height: 5)
.foregroundColor(colorArray[item.color])
Text(item.subText2)
.bold()
.foregroundColor (.white)
.font(.subheadline)
Spacer()
}
Spacer()
}
}
.padding(.horizontal)
}
}
struct item: Identifiable {
var id = UUID().uuidString
// both Image And Name Are Same....
var name: String
// since all Are Apple Native Apps...
var color: Int
var subText: String
var subText2: String
}
var searchItems = [
item(name: "Untitled (Skull)", color: 0, subText: "1983", subText2: "yay"),
item(name: "Dogs", color: 1, subText: "1972", subText2: "wow"),
item(name: "Cats", color: 2, subText: "1968", subText2: "oof")
]
struct Search_Bar: View {
#State var filteredItems = searchItems
var body: some View {
CustomNavigationView(view: AnyView(Home(filteredItems: $filteredItems)), placeHolder: "Museums, Art or anything else.", largeTitle: true, title: "Search",
onSearch: { (txt) in
if txt != ""{
self.filteredItems = searchItems.filter{$0.name.lowercased().contains(txt.lowercased())}
}
else{
self.filteredItems = searchItems
}
}, onCancel: {
// Do Your Own Code When Search And Canceled....
self.filteredItems = searchItems
})
.ignoresSafeArea()
}
}
struct Search_Bar_Previews: PreviewProvider {
static var previews: some View {
Search_Bar()
}
}
import SwiftUI
struct CustomNavigationView: UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
return CustomNavigationView.Coordinator(parent: self)
}
// Just Change Your View That Requires Search Bar...
var view: AnyView
// Ease Of Use.....
var largeTitle: Bool
var title: String
var placeHolder: String
// onSearch And OnCancel Closures....
var onSearch: (String)->()
var onCancel: ()->()
// requre closure on Call...
init(view: AnyView,placeHolder: String? = "Search",largeTitle: Bool? = true,title: String,onSearch: #escaping (String)->(),onCancel: #escaping ()->()) {
self.title = title
self.largeTitle = largeTitle!
self.placeHolder = placeHolder!
self.view = view
self.onSearch = onSearch
self.onCancel = onCancel
}
// Integrating UIKit Navigation Controller With SwiftUI View...
func makeUIViewController(context: Context) -> UINavigationController {
// requires SwiftUI View...
let childView = UIHostingController(rootView: view)
let controller = UINavigationController(rootViewController: childView)
// Nav Bar Data...
controller.navigationBar.topItem?.title = title
controller.navigationBar.prefersLargeTitles = largeTitle
// search Bar....
let searchController = UISearchController()
searchController.searchBar.placeholder = placeHolder
// setting delegate...
searchController.searchBar.delegate = context.coordinator
// setting Search Bar In NavBar...
// disabling hide on scroll...
// disabling dim bg..
searchController.obscuresBackgroundDuringPresentation = false
controller.navigationBar.topItem?.hidesSearchBarWhenScrolling = false
controller.navigationBar.topItem?.searchController = searchController
return controller
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
// Updating Real Time...
uiViewController.navigationBar.topItem?.title = title
uiViewController.navigationBar.topItem?.searchController?.searchBar.placeholder = placeHolder
uiViewController.navigationBar.prefersLargeTitles = largeTitle
}
// search Bar Delegate...
class Coordinator: NSObject,UISearchBarDelegate{
var parent: CustomNavigationView
init(parent: CustomNavigationView) {
self.parent = parent
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
// when text changes....
self.parent.onSearch(searchText)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
// when cancel button is clicked...
self.parent.onCancel()
}
}
}
Letting the random view below for the array being for example:
import SwiftUI
struct cats: View {
var body: some View {
Text("cats") //replacing this with dogs or untitled skull as an example.
}
}
struct cats_Previews: PreviewProvider {
static var previews: some View {
cats()
}
}
You can use ForEach getting the item and its index in the closure :
ForEach(Array(filteredItems.enumerated()), id: \.1.id) { index, item in
NavigationLink(destination: views[index]){
Text(item.name)
}
}
For example :
struct ListItem: Identifiable {
let id = UUID()
let name: String
}
struct SwiftUIView17: View {
#State private var filteredItems = ["John", "Bob", "Maria"].map(ListItem.init)
let views = [AnyView(Text("John destination")), AnyView(Text("Bob destination")), AnyView(Text("Maria destination"))]
var body: some View {
ScrollView {
ForEach(Array(filteredItems.enumerated()), id: \.1.id) { index, item in
NavigationLink(destination: views[index]){
Text(item.name)
}
}
}
}
}
But it would be better not to use AnyView but a ViewBuilder :
struct SwiftUIView17: View {
#State private var filteredItems = ["John", "Bob", "Maria"].map(ListItem.init)
#ViewBuilder func destination(for itemIndex: Int) -> some View {
switch itemIndex {
case 0: Text("John destination")
case 1: Text("Bob destination").foregroundColor(.red)
case 2: Rectangle()
default: Text("error")
}
}
var body: some View {
ScrollView {
ForEach(Array(filteredItems.enumerated()), id: \.1.id) { index, item in
NavigationLink(destination: destination(for: index)){
Text(item.name)
}
}
}
}
}

SwiftUI: Switch between List and grid/Collection View

Goal: A button that switches between List and Grid/Collection View.
For this, I am using the great WaterfallGrid:
https://github.com/paololeonardi/WaterfallGrid
I haven't managed able to make it work. I am using state, and if statement, as code bellow:
import SwiftUI
import WaterfallGrid
struct Fruit: Identifiable {
let id = UUID()
let name: String
let image: Image
}
struct ExampleView: View {
#State private var fruits = [
Fruit(name: "Apple", image: Image("apple")),
Fruit(name: "Banana", image: Image("banana")),
Fruit(name: "Grapes", image: Image("grapes")),
Fruit(name: "Peach", image: Image("peach"))]
#State private var showgrid = true
var body: some View {
NavigationView {
if showgrid == .true {
return
WaterfallGrid(fruits) { fruit in
HStack {
fruit.image.resizable().frame(width: 30, height: 30)
Text(fruit.name)
}
}
}
else {
return
List(fruits) { fruit in
HStack {
fruit.image.resizable().frame(width: 30, height: 30)
Text(fruit.name)
}
}
}
.navigationBarTitle("Fruits")
.navigationBarItems(trailing:
Button(action: { self.showmaterialrmenu.toggle() }) {
Image(systemName: "rectangle.on.rectangle.angled")
})
}
}
}
struct ExampleView_Previews: PreviewProvider {
static var previews: some View {
ExampleView()
}
}
Really appreciate any help!
Cheers
Hello it's not a good way to put all you views in one place I created one project that need to be having a list and grid mode so here's my implementation:
private enum HomeMode {
case list, grid
func icon() -> String {
switch self {
case .list: return "rectangle.3.offgrid.fill"
case .grid: return "rectangle.grid.1x2"
}
}
}
#State private var homeMode = HomeMode.list
private var swapHomeButton: some View {
Button(action: {
self.homeMode = self.homeMode == .grid ? .list : .grid
}) {
HStack {
Image(systemName: self.homeMode.icon()).imageScale(.medium)
}.frame(width: 30, height: 30)
}
}
and here's my main view:
var body: some View {
let view = Group {
if homeMode == .list {
homeAsList
} else {
homeAsGrid
}
}
.navigationBarItems(trailing:
HStack {
swapHomeButton
settingButton
}
).sheet(isPresented: $isSettingPresented,
content: { SettingsForm() })
return navigationView(content: AnyView(view))
}
HomeList:
private var homeAsList: some View {
Group {
if selectedMenu.menu == .genres {
GenresList(headerView: AnyView(segmentedView))
} else {
MoviesHomeList(menu: $selectedMenu.menu,
pageListener: selectedMenu.pageListener,
headerView: AnyView(segmentedView))
}
}
}
private var homeAsGrid: some View {
MoviesHomeGrid()
}
that's the answer If you are asking about how to arrange the views but if you are asking how to implement the WaterfallGrid hit me back so I can check it and see if we can solve this