NSTableView with scroll view and header view in SwiftUI - swiftui

I'm trying to implement an NSTableView in a SwiftUI app in MacOS using NSViewRepresentable. The part of creating the table view works well so far:
struct TableView: NSViewRepresentable {
typealias NSViewType = NSTableView
func makeNSView(context: Context) -> NSTableView {
let tableView = NSTableView(frame: .zero)
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
tableView.rowHeight = 20.0
tableView.gridStyleMask = [.solidVerticalGridLineMask, .solidHorizontalGridLineMask]
let nameColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: "Name"))
nameColumn.title = "Name"
tableView.addTableColumn(nameColumn)
let ageColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: "Age"))
ageColumn.title = "Age"
tableView.addTableColumn(ageColumn)
return tableView
}
func updateNSView(_ nsView: NSTableView, context: Context) {
nsView.reloadData()
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
}
extension TableView {
final class Coordinator: NSObject, NSTableViewDelegate, NSTableViewDataSource {
func numberOfRows(in tableView: NSTableView) -> Int {
return 12
}
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
switch tableColumn?.identifier.rawValue {
case "Name":
return "Person \(row)"
case "Age":
return "\(25 + row)"
default:
return nil
}
}
func tableView(_ tableView: NSTableView, shouldEdit tableColumn: NSTableColumn?, row: Int) -> Bool {
return true
}
func tableView(_ tableView: NSTableView, setObjectValue object: Any?, for tableColumn: NSTableColumn?, row: Int) {
}
}
}
Of course, this only gives me the pure table, without a NSScrollView or a NSHeaderView.
My question is now: which would be the most elegant and SwiftUI-ish way to integrate NSScrollView and NSHeaderView?
bundle all three with NSViewRepresentable (very monolithic)
pull the NSHeaderView from my NSViewRepresentable and somehow set it up inside SwiftUI's ScrollView (not sure how I would achieve that it only scrolls horizontally though)
something else?
Thanks!

Thanks for you question, I used your code as base for my solution.
Basically to show the NSTable header you need to wrap the NSTableView in a ScrollView.
This is my final code
struct TableView: NSViewRepresentable {
typealias NSViewType = NSScrollView
func makeNSView(context: Context) -> NSScrollView {
let tableContainer = NSScrollView(frame: .zero)
let tableView = NSTableView(frame: .zero)
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
tableView.rowHeight = 20.0
tableView.gridStyleMask = [.solidVerticalGridLineMask, .solidHorizontalGridLineMask]
let nameColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: "Name"))
nameColumn.title = "Name"
tableView.addTableColumn(nameColumn)
let ageColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: "Age"))
ageColumn.title = "Age"
tableView.addTableColumn(ageColumn)
tableContainer.documentView = tableView
tableContainer.hasVerticalScroller = true
return tableContainer
}
func updateNSView(_ nsView: NSScrollView, context: Context) {
guard let tableView = nsView.documentView as? NSTableView else { return }
tableView.reloadData()
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
}

Related

Setting up .onCommit{} in UITextFieldViewRepresentable SwiftUI

I bridge UIKit with SwiftUI as follows:
struct UITextFieldViewRepresentable: UIViewRepresentable {
#Binding var language: String
#Binding var text: String
init(language: Binding<String>, text: Binding<String>) {
self._language = language
self._text = text
}
func makeUIView(context: Context) -> UITextField {
let textField = getTextField()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
// Change the language in the wordTextField here.
if let wordTextField = uiView as? WordTextField {
wordTextField.language = self.language
}
}
private func getTextField() -> UITextField {
let textField = WordTextField(frame: .zero)
textField.language = self.language
textField.textAlignment = .center
textField.font = UIFont.systemFont(ofSize: 15, weight: .regular)
return textField
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text)
}
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
init(text: Binding<String>) {
self._text = text
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
}
class WordTextField: UITextField {
var language: String? {
didSet {
if self.isFirstResponder{
self.resignFirstResponder()
self.becomeFirstResponder()
}
}
}
override var textInputMode: UITextInputMode? {
if let language = self.language {
print("text input mode: \(language)")
for inputMode in UITextInputMode.activeInputModes {
if let inputModeLanguage = inputMode.primaryLanguage, inputModeLanguage == language {
return inputMode
}
}
}
return super.textInputMode
}
}
}
And call it as follows:
UITextFieldViewRepresentable(language: $keyboardLanguage, text: $foreignThing)
This works fine in some parts of my app. In other parts, I need a text field which calls a method when the user taps the enter key after entering text. It's written like this:
TextField("", text: Binding<String>(
get: { self.userAnswer },
set: {
self.userAnswer = $0
self.enableHint()
}), onCommit: {
if self.userAnswer.isEmpty {
answerPlaceholder = NSMutableAttributedString(string: "Tap here to answer...")
} else {
answerDisabled = true
checkAnswer()
}
})
I tried implementing the above with UITextFieldViewRepresenatable as follows:
UITextFieldViewRepresentable(language: $keyboardLanguage, text: Binding<String>(
get: { self.userAnswer },
set: {
self.userAnswer = $0
self.enableHint()
}), onCommit: {
if self.userAnswer.isEmpty {
answerPlaceholder = NSMutableAttributedString(string: "Tap here to answer...")
} else {
answerDisabled = true
checkAnswer()
}
})
I'm getting 'compiler couldn't type check this expression in reasonable time' error. I think I've narrowed it down to not implementing .onCommit:{} in my UITextFieldViewRepresentable()
If this is the problem, then I'd like to know how .onCommit:{} can be implemented in UITextFieldViewRepresentable().
There are a few mistakes in the UIViewRepresentable implementation:
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text)
}
This text binding will be out of date when the View is recreated, so change it to:
func makeCoordinator() -> Coordinator {
return Coordinator()
}
You can store the textField inside the coordinator, make it a lazy, set delegate self, then return it from the make func, eg..
func makeUIView(context: Context) -> UITextField {
context.coordinator.textField // lazy property that sets delegate self.
}
In update, you need to make use of the new value of the binding, e.g.
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
// Change the language in the wordTextField here.
if let wordTextField = uiView as? WordTextField {
wordTextField.language = self.language
}
// use the new binding
context.coordinator.textDidChange = { newText in
text = newText
}
}
class Coordinator: NSObject, UITextFieldDelegate {
lazy var textField: UITextField = {
let textField = UITextField()
textField.delegate = self
return textField
}()
var textDidChange: ((String) -> Void)?
func textFieldDidChangeSelection(_ textField: UITextField) {
textDidChange?(textField.text)
}
}
You'll see something similar in PaymentButton.swift in Apple's Fruta sample.
Perhaps a simpler way would be this (but I've not tested it yet):
// update
context.coordinator.textBinding = _text
// Coordinator
var textBinding: Binding<String>?
// textViewDidChange
textBinding?.wrappedValue = textField.text

Disable horizontal scrolling for WKWebView

I know how to do this for UIWebView, but it is deprecated. I have figured out how to hide both the vertical and horizontal scroll indicators, disable scrollview bounces and disable the pinch gesture recognizer but still haven't found a way to wholly disable horizontal scrolling in the webview. Any help would be appreciated, below is my WebView.Swift.
struct WebView: UIViewRepresentable
{
let request: URLRequest
var webView: WKWebView?
init (request: URLRequest)
{
self.webView = WKWebView()
self.request = request
webView?.scrollView.showsHorizontalScrollIndicator = false
webView?.scrollView.showsVerticalScrollIndicator = false
webView?.scrollView.pinchGestureRecognizer?.isEnabled = false
webView?.scrollView.bounces = false
}
func makeUIView(context: Context) -> WKWebView {
return webView!
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(request)
}
func goBack()
{
webView?.goBack()
}
func refresh()
{
webView?.reload()
}
func goHome()
{
webView?.load(request)
}
}
For this, you may use Coordinator. There is good explanation for their.
Create class Coordinator in your UIViewRepresentable. Add UIScrollViewDelegate to class. In makeUIView, set webView?.scrollView.delegate = context.coordinator.
In Coordinator, you need this function.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (scrollView.contentOffset.x > 0){
scrollView.contentOffset = CGPoint(x: 0, y: scrollView.contentOffset.y)
}
}
And now, horizontal scroll not work!
All code
import WebKit
struct WebView: UIViewRepresentable {
let request: URLRequest
var webView: WKWebView?
class Coordinator: NSObject, UIScrollViewDelegate {
var parent: WebView
init(_ parent: WebView) {
self.parent = parent
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (scrollView.contentOffset.x > 0){
scrollView.contentOffset = CGPoint(x: 0, y: scrollView.contentOffset.y)
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
init (request: URLRequest) {
self.webView = WKWebView()
self.request = request
webView?.scrollView.showsHorizontalScrollIndicator = false
webView?.scrollView.showsVerticalScrollIndicator = false
webView?.scrollView.pinchGestureRecognizer?.isEnabled = false
webView?.scrollView.bounces = false
}
func makeUIView(context: Context) -> WKWebView {
webView?.scrollView.delegate = context.coordinator
return webView!
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(request)
}
// You funcs
}

UITextField not updating to change in ObservableObject (SwiftUI)

I am trying to make a UITextView that edits a value currentDisplayedAddress. The value is also changed by other views, and I want the UITextView to update its text when that occurs.
The view initializes correctly, and I can edit currentDisplayedAddress from AddressTextField with no problem and trigger relevant view updates. However, when the value is changed by other views, the textField's text does not change, even though textField.text prints the correct updated value inside updateUIView and other views update accordingly.
I have no idea what may have caused this. Any help is extremely appreciated.
struct AddressTextField: UIViewRepresentable {
private let textField = UITextField(frame: .zero)
var commit: () -> Void
#EnvironmentObject var userData: UserDataModel
func makeUIView(context: UIViewRepresentableContext<AddressTextField>) -> UITextField {
textField.text = self.userData.currentDisplayedAddress
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<AddressTextField>) {
if self.textField.text != self.userData.currentDisplayedAddress {
DispatchQueue.main.async {
self.textField.text = self.userData.currentDisplayedAddress
}
}
}
(...)
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject, UITextFieldDelegate {
var addressTextField: AddressTextField
func textFieldShouldReturn(_ textField: UITextField) -> Bool { //delegate method
textField.resignFirstResponder()
addressTextField.userData.currentDisplayedAddress = textField.text ?? String()
addressTextField.commit()
return true
}
}
}
You should not create UITextField as property, because AddressTextField struct can be recreated during parent update. The makeUIView is exactly the place to create UI-instances, the representable is handling its reference on your behalf.
So here is fixed variant. Tested with Xcode 11.4 / iOS 13.4
struct AddressTextField: UIViewRepresentable {
var commit: () -> Void = {}
#EnvironmentObject var userData: UserDataModel
func makeUIView(context: UIViewRepresentableContext<AddressTextField>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.text = self.userData.currentDisplayedAddress
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ textField: UITextField, context: UIViewRepresentableContext<AddressTextField>) {
if textField.text != self.userData.currentDisplayedAddress {
textField.text = self.userData.currentDisplayedAddress
}
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject, UITextFieldDelegate {
var addressTextField: AddressTextField
init(_ addressTextField: AddressTextField) {
self.addressTextField = addressTextField
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool { //delegate method
textField.resignFirstResponder()
addressTextField.userData.currentDisplayedAddress = textField.text ?? String()
addressTextField.commit()
return true
}
}
}

SwiftUI: Implementing wrapper around UITableView to achieve custom List-like view

I would like to implement custom List-like view in SwiftUI that has to enable more feature than standard native List view in SwiftUI. I want to add drag and drop which doesn't exist in List (in spite of onMove()).
I have implemented this list in such way:
import SwiftUI
import MobileCoreServices
final class ReorderIndexPath: NSIndexPath {
}
extension ReorderIndexPath : NSItemProviderWriting {
public static var writableTypeIdentifiersForItemProvider: [String] {
return [kUTTypeData as String]
}
public func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: #escaping (Data?, Error?) -> Void) -> Progress? {
let progress = Progress(totalUnitCount: 100)
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false)
progress.completedUnitCount = 100
completionHandler(data, nil)
} catch {
completionHandler(nil, error)
}
return progress
}
}
extension ReorderIndexPath : NSItemProviderReading {
public static var readableTypeIdentifiersForItemProvider: [String] {
return [kUTTypeData as String]
}
public static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> ReorderIndexPath {
do {
return try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! ReorderIndexPath
} catch {
fatalError(error.localizedDescription)
}
}
}
struct ReorderableList: UIViewControllerRepresentable {
struct Model {
private(set) var items : [AnyView]
init(items: [AnyView]) {
self.items = items
}
mutating func addItem(_ item: AnyView, at index: Int) {
items.insert(item, at: index)
}
mutating func removeItem(at index: Int) {
items.remove(at: index)
}
mutating func moveItem(at sourceIndex: Int, to destinationIndex: Int) {
guard sourceIndex != destinationIndex else { return }
let item = items[sourceIndex]
items.remove(at: sourceIndex)
items.insert(item, at: destinationIndex)
}
func canHandle(_ session: UIDropSession) -> Bool {
return session.canLoadObjects(ofClass: ReorderIndexPath.self)
}
func dragItems(for indexPath: IndexPath) -> [UIDragItem] {
//let item = items[indexPath.row]
//let data = item.data(using: .utf8)
let itemProvider = NSItemProvider()
itemProvider.registerObject(ReorderIndexPath(row: indexPath.row, section: indexPath.section), visibility: .all)
return [
UIDragItem(itemProvider: itemProvider)
]
}
}
#State private var model : Model
// MARK: - Actions
let onReorder : (Int, Int) -> Void
let onDelete : ((Int) -> Bool)?
// MARK: - Init
public init<Data, RowContent>(onReorder: #escaping (Int, Int) -> Void = { _, _ in }, onDelete: ((Int) -> Bool)? = nil, _ content: #escaping () -> ForEach<Data, Data.Element.ID, RowContent>) where Data : RandomAccessCollection, RowContent : View, Data.Element : Identifiable {
let content = content()
var items = [AnyView]()
content.data.forEach { element in
let item = content.content(element)
items.append(AnyView(item))
}
self.onReorder = onReorder
self.onDelete = onDelete
self._model = State(initialValue: Model(items: items))
}
public init<Data, RowContent>(onReorder: #escaping (Int, Int) -> Void = { _,_ in }, onDelete: ((Int) -> Bool)? = nil, _ data: Data, #ViewBuilder rowContent: #escaping (Data.Element) -> RowContent) where Data : RandomAccessCollection, RowContent : View, Data.Element : Identifiable {
self.init(onReorder: onReorder, onDelete: onDelete) {
ForEach(data) { element in HStack { rowContent(element) } }
}
}
public init<Data, ID, RowContent>(onReorder: #escaping (Int, Int) -> Void = { _,_ in }, onDelete: ((Int) -> Bool)? = nil, _ content: #escaping () -> ForEach<Data, ID, RowContent>) where Data : RandomAccessCollection, ID : Hashable, RowContent : View {
let content = content()
var items = [AnyView]()
content.data.forEach { element in
let item = content.content(element)
items.append(AnyView(item))
}
self.onReorder = onReorder
self.onDelete = onDelete
self._model = State(initialValue: Model(items: items))
}
public init<Data, ID, RowContent>(onReorder: #escaping (Int, Int) -> Void = { _,_ in }, onDelete: ((Int) -> Bool)? = nil, _ data: Data, id: KeyPath<Data.Element, ID>, #ViewBuilder rowContent: #escaping (Data.Element) -> RowContent) where Data : RandomAccessCollection, ID : Hashable, RowContent : View {
self.init(onReorder: onReorder, onDelete: onDelete) {
ForEach(data, id: id) { element in HStack { rowContent(element) } }
}
}
public init<RowContent>(onReorder: #escaping (Int, Int) -> Void = { _,_ in }, onDelete: ((Int) -> Bool)? = nil, _ content: #escaping () -> ForEach<Range<Int>, Int, RowContent>) where RowContent : View {
let content = content()
var items = [AnyView]()
content.data.forEach { i in
let item = content.content(i)
items.append(AnyView(item))
}
self.onReorder = onReorder
self.onDelete = onDelete
self._model = State(initialValue: Model(items: items))
}
public init<RowContent>(onReorder: #escaping (Int, Int) -> Void = {_,_ in }, onDelete: ((Int) -> Bool)? = nil, _ data: Range<Int>, #ViewBuilder rowContent: #escaping (Int) -> RowContent) where RowContent : View {
self.init(onReorder: onReorder, onDelete: onDelete) {
ForEach(data) { i in
HStack { rowContent(i) }
}
}
}
func makeUIViewController(context: Context) -> UITableViewController {
let tableView = UITableViewController()
tableView.tableView.delegate = context.coordinator
tableView.tableView.dataSource = context.coordinator
tableView.tableView.dragInteractionEnabled = true
tableView.tableView.dragDelegate = context.coordinator
tableView.tableView.dropDelegate = context.coordinator
tableView.tableView.register(HostingTableViewCell<AnyView>.self, forCellReuseIdentifier: "HostingCell")
context.coordinator.controller = tableView
return tableView
}
func updateUIViewController(_ uiView: UITableViewController, context: Context) {
//print("Reorderable list update")
//uiView.tableView.reloadData()
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITableViewDelegate, UITableViewDataSource, UITableViewDragDelegate, UITableViewDropDelegate {
let parent: ReorderableList
weak var controller : UITableViewController?
// MARK: - Init
init(_ parent: ReorderableList) {
self.parent = parent
}
// MARK: - Data Source
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
parent.model.items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "HostingCell") as! HostingTableViewCell<AnyView>
let item = parent.model.items[indexPath.row]
cell.host(item, parent: controller!)
return cell
}
// MARK: - Delegate
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return parent.onDelete != nil ? .delete : .none
}
func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
return false
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
if parent.onDelete?(indexPath.row) ?? false {
tableView.beginUpdates()
parent.model.removeItem(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
tableView.endUpdates()
}
} else if editingStyle == .insert {
}
}
/*
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let object = parent.model.items[sourceIndexPath.row]
parent.model.items.remove(at: sourceIndexPath.row)
parent.model.items.insert(object, at: destinationIndexPath.row)
}
*/
// MARK: - Drag Delegate
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
return parent.model.dragItems(for: indexPath)
}
// MARK: - Drop Delegate
func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool {
return parent.model.canHandle(session)
}
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
if tableView.hasActiveDrag {
if session.items.count > 1 {
return UITableViewDropProposal(operation: .cancel)
} else {
return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
} else {
return UITableViewDropProposal(operation: .copy, intent: .insertAtDestinationIndexPath)
}
}
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
let destinationIndexPath: IndexPath
if let indexPath = coordinator.destinationIndexPath {
destinationIndexPath = indexPath
} else {
// Get last index path of table view.
let section = tableView.numberOfSections - 1
let row = tableView.numberOfRows(inSection: section)
destinationIndexPath = IndexPath(row: row, section: section)
}
coordinator.session.loadObjects(ofClass: ReorderIndexPath.self) { items in
// Consume drag items.
let indexPaths = items as! [IndexPath]
for (index, sourceIndexPath) in indexPaths.enumerated() {
let destinationIndexPath = IndexPath(row: destinationIndexPath.row + index, section: destinationIndexPath.section)
self.parent.model.moveItem(at: sourceIndexPath.row, to: destinationIndexPath.row)
tableView.moveRow(at: sourceIndexPath, to: destinationIndexPath)
self.parent.onReorder(sourceIndexPath.row, destinationIndexPath.row)
}
}
}
}
}
And here is client code using it
struct ContentView: View {
#State private var items: [String] = ["Item 1", "Item 2", "Item 3"]
var body: some View {
NavigationView {
ReorderableList(onReorder: reorder, onDelete: delete) {
ForEach(self.items, id: \.self) { item in
Text("\(item)")
}
}
.navigationBarTitle("Reorderable List", displayMode: .inline)
.navigationBarItems(trailing: Button(action: add, label: {
Image(systemName: "plus")
}))
}
}
func reorder(from source: Int, to destination: Int) {
items.move(fromOffsets: IndexSet([source]), toOffset: destination)
}
func delete(_ idx: Int) -> Bool {
items.remove(at: idx)
return true
}
func add() {
items.append("Item \(items.count)")
}
}
Problem is that it doesn't have natural refreshing behaviour of List so tapping + button in navbar and adding items does not refresh my ReorderableList
UPDATE
I have also simplified example to test this refreshing of cells as above code is a little long.
struct ReorderableList2<T, Content>: UIViewRepresentable where Content : View {
#Binding private var items: [T]
let content: (T) -> Content
init(_ items: Binding<[T]>, #ViewBuilder content: #escaping (T) -> Content) {
self.content = content
self._items = items
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITableView {
let tableView = UITableView()
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
tableView.register(HostingTableViewCell.self, forCellReuseIdentifier: "HostingCell")
return tableView
}
func updateUIView(_ uiView: UITableView, context: Context) {
uiView.reloadData()
}
class Coordinator : NSObject, UITableViewDataSource, UITableViewDelegate {
private let parent: ReorderableList2
// MARK: - Init
init(_ parent: ReorderableList2) {
self.parent = parent
}
// MARK: - Data Source
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
parent.items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "HostingCell") as! HostingTableViewCell
let item = parent.items[indexPath.row]
let rootView = parent.content(item)
cell.host(rootView: rootView)
return cell
}
// MARK: - Delegate
}
}
This simplified version also does not work, even when Binding items has new items added to it.
tableView:numberOfRowsInSection: is called correctly each time I am adding new item but parent.items.count is wrong old number
▿ ReorderableList2<String, Text>
▿ _items : Binding<Array<String>>
▿ transaction : Transaction
▿ plist : []
- elements : nil
▿ location : <LocationBox<ScopedLocation>: 0x6000016a4bd0>
▿ _value : 3 elements
- 0 : "Item 1"
- 1 : "Item 2"
- 2 : "Item 3"
- content : (Function)
even though in constructor or in updateUIView() checking this same items Binding gives correct updated items list.
▿ ReorderableList2<String, Text>
▿ _items : Binding<Array<String>>
▿ transaction : Transaction
▿ plist : []
- elements : nil
▿ location : <LocationBox<ScopedLocation>: 0x6000016a4bd0>
▿ _value : 5 elements
- 0 : "Item 1"
- 1 : "Item 2"
- 2 : "Item 3"
- 3 : "Item 3"
- 4 : "Item 4"
- content : (Function)
I've found such trick, but doesn't like this DispatchQueue.main.async { } mutating state in updateUIView() If someone has a better idea how to solve this problem leave other solution in comments.
As I found:
View is struct so each time its initializer is called and there are created new references to items/model properties despite them being class or struct
makeCoordinator() is called only once so when there is redrawing there is old Coordinator with old references
as we know #State is kept between view redrawings as it has some underlying storage related with it so in each redrawn View model references (different pointers) read from the same underlying storage. So updating in updateUIView() this #State refreshes this state on all referencing paths including one that Coordinator is keeping via immutable parent View reference.
import SwiftUI
extension ReorderableList2 {
struct Model<T> {
private(set) var items: [T]
init(items: [T]) {
self.items = items
}
mutating func addItem(_ item: T, at index: Int) {
items.insert(item, at: index)
}
mutating func removeItem(at index: Int) {
items.remove(at: index)
}
mutating func moveItem(at source: Int, to destination: Int) {
guard source != destination else { return }
let item = items[source]
items.remove(at: source)
items.insert(item, at: destination)
}
mutating func replaceItems(_ items: [T]) {
self.items = items
}
}
}
struct ReorderableList2: UIViewRepresentable where Content : View {
// MARK: - State
#State private(set) var model = Model<T>(items: [])
// MARK: - Properties
private let items: [T]
private let content: (T) -> Content
init(_ items: [T], #ViewBuilder content: #escaping (T) -> Content) {
self.content = content
self.items = items
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITableView {
let tableView = UITableView()
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
tableView.register(HostingTableViewCell.self, forCellReuseIdentifier: "HostingCell")
return tableView
}
func updateUIView(_ uiView: UITableView, context: Context) {
DispatchQueue.main.async {
self.model.replaceItems(self.items)
uiView.reloadData()
}
}
class Coordinator : NSObject, UITableViewDataSource, UITableViewDelegate {
private let parent: ReorderableList2
// MARK: - Init
init(_ parent: ReorderableList2) {
self.parent = parent
}
// MARK: - Data Source
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
parent.model.items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "HostingCell") as! HostingTableViewCell
let item = parent.model.items[indexPath.row]
let rootView = parent.content(item)
cell.host(rootView: rootView)
return cell
}
// MARK: - Delegate
}
}

Presenting image picker using TapGesture on UIImageView

I have a View with an UIImageView that I want to be 'selectable' so that the user can pick a new image.
The function for picking the new image is in the Controller.
Question
How do I call the myDatasourceController.handleTap() function by pressing the ImageView, so that the image picker is presented?
This is an example of my current setup
View
class myView: UICollectionViewCell {
lazy var profileImageView: UIImageView = {
let iv = UIImageView()
iv.isUserInteractionEnabled = true
iv.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(myDatasourceController.handleTap)))
return iv
}()
}
Controller
class myDatasourceController: UICollectionViewController,
UICollectionViewDelegateFlowLayout, UIImagePickerControllerDelegate,
UINavigationControllerDelegate {
func handleTap(){
let imagePickerController = UIImagePickerController()
imagePickerController.delegate = self
imagePickerController.allowsEditing = true
present(imagePickerController, animated: true, completion: nil)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
// logic for picking the image
dismiss(animated: true, completion: nil)
}
}
This setup currently throws the error
unrecognized selector sent to instance 0x7f9163d493f0
which has led me to try various combinations of
handleTap(_:)
handleTap(sender: UITapGestureRecogniser)
/// etc
but I can't get any of them to work. How should I be constructing my View, Controller, and the interaction between them to present the image picker?
Use Like this
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(RegisterViewController. handleTap(gesture:)))
func handleTap(gesture: UIGestureRecognizer) {
// if the tapped view is a UIImageView then set it to imageview
if (gesture.view as? UIImageView) != nil {
print("Image Tapped")
picker.allowsEditing = false
picker.sourceType = .photoLibrary
picker.mediaTypes = UIImagePickerController.availableMediaTypes(for: .photoLibrary)!
present(picker, animated: true, completion: nil)
}
}
Use like this :
myDatasourceController.handleTap()
In your code :
iv.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(myDatasourceController.handleTap())))
The key to the solution is to implement a protocol / delegate, as suggested by #Akis
I've uploaded the full project to my github account. The key code is copied here.
View Controller
protocol ImagePickerDelegate: class {
func loadImagePicker()
}
class HomeViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, UIImagePickerControllerDelegate, UINavigationControllerDelegate, ImagePickerDelegate {
let cellId = "cellId"
func loadImagePicker(){
print(" -- image picker -- ")
// load image picker
let imagePickerController = UIImagePickerController()
imagePickerController.delegate = self
imagePickerController.allowsEditing = true
present(imagePickerController, animated: true, completion: nil)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismiss(animated: true, completion: nil)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
// get the image
var selectedImageFromPicker: UIImage?
if let editedImage = info["UIImagePickerControllerEditedImage"] as? UIImage {
selectedImageFromPicker = editedImage
}else if let originalImage = info["UIImagePickerControllerOriginalImage"] as? UIImage {
selectedImageFromPicker = originalImage
}
if let selectedImage = selectedImageFromPicker {
//doSomethingWithTheImage(image: selectedImage)
}
dismiss(animated: true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.backgroundColor = .black
collectionView?.register(HomeView.self, forCellWithReuseIdentifier: cellId)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! HomeView
cell.delegate = self
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: view.frame.height)
}
}
View
class HomeView: UICollectionViewCell {
// break retain cycle with weak var
weak var delegate: ImagePickerDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
lazy var profileImageView: UIImageView = {
let iv = UIImageView()
iv.isUserInteractionEnabled = true
iv.image = UIImage(named: "kuang-si-falls-waterfall-water-laos-50588.jpg")
iv.contentMode = .scaleAspectFill
let tap = UITapGestureRecognizer(target: self, action: #selector(loadImagePicker))
iv.addGestureRecognizer(tap)
return iv
}()
func loadImagePicker() {
delegate?.loadImagePicker()
print(" imagePickerProtocol called ")
}
func setupViews() {
backgroundColor = .white
addSubview(profileImageView)
profileImageView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
profileImageView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
profileImageView.frame = CGRect(x: 0, y: 0, width: 150, height: 150)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}