Using SwiftUI View for UICollectionViewCell - swiftui

I made UICollectionView that accept (Item) -> Content (where Content:View) as init parameter to pass this SwiftUI View to cell content view using UIHostController. But by some reason my screen is empty, however if I pass some View instead of Content directly to UIHosting controller everything works fine.
Code:
Cell
final class SnapCarouselCell<Content:View>: UICollectionViewCell{
var cellContent: Content?
override init(frame: CGRect) {
super.init(frame: frame)
let vc = UIHostingController(rootView: cellContent)
contentView.addSubview(vc.view)
vc.view.translatesAutoresizingMaskIntoConstraints = false
vc.view.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
vc.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
vc.view.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true
vc.view.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Controller
class SnapCarouselViewController<Item: Equatable, Content: View>: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource{
lazy var snapCarouselView: UICollectionView = {
let collectionView = setUpCollectionView()
self.view.addSubview(collectionView)
return collectionView
}()
private let flowLayout: UICollectionViewFlowLayout
private let cellHeight: CGFloat
private let cellWidth: CGFloat
private var centerCell: UICollectionViewCell?
private let items: [Item]
private let cellContent: (Item) -> Content
init(
cellHeight: CGFloat,
cellWidth: CGFloat,
items: [Item],
#ViewBuilder cellContent: #escaping (Item) -> Content
){
self.cellHeight = cellHeight
self.cellWidth = cellWidth
self.flowLayout = SnapCarouselViewFlowLayout(cellWidth: cellWidth, cellHeight: cellHeight)
self.items = items
self.cellContent = cellContent
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
snapCarouselView.dataSource = self
snapCarouselView.delegate = self
let indexPath = IndexPath(row: 9999999999, section: 0)
snapCarouselView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return Int.max
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SnapCarouselCell", for: indexPath) as! SnapCarouselCell<Content>
cell.cellContent = cellContent(items[indexPath.row % items.count])
return cell
}
private func setUpCollectionView() -> UICollectionView{
let collectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: flowLayout)
collectionView.backgroundColor = .white
collectionView.showsHorizontalScrollIndicator = false
collectionView.frame = CGRect(x: 0, y: 100, width: view.frame.size.width, height: cellHeight)
collectionView.decelerationRate = .fast
collectionView.register(SnapCarouselCell<Content>.self, forCellWithReuseIdentifier: "SnapCarouselCell")
return collectionView
}
}
View
struct SnapCarouselView<Content: View, Item: Equatable>: UIViewControllerRepresentable {
private let cellContent: (Item) -> Content
private let cellHeight: CGFloat
private let cellWidth: CGFloat
private let items: [Item]
init(
cellHeight: CGFloat,
cellWidth: CGFloat,
items: [Item],
#ViewBuilder cellContent: #escaping (Item) -> Content
) {
self.cellHeight = cellHeight
self.cellWidth = cellWidth
self.cellContent = cellContent
self.items = items
}
func makeUIViewController(context: Context) -> SnapCarouselViewController<Item,Content> {
let vc = SnapCarouselViewController(
cellHeight: cellHeight,
cellWidth: cellWidth,
items: items,
cellContent: cellContent)
return vc
}
func updateUIViewController(_ uiViewController: SnapCarouselViewController<Item,Content>, context: Context) {
}
typealias UIViewControllerType = SnapCarouselViewController
}
struct TestUICollectionView_Previews: PreviewProvider {
static var previews: some View {
SnapCarouselView(cellHeight: 200, cellWidth: 200, items: test) { item in
Text(item.name)
}
}
}
struct Test: Equatable{
var id = UUID()
let name : String
}
let test = [
Test(name: "1"),
Test(name: "2"),
Test(name: "3"),
Test(name: "4"),
Test(name: "5"),
Test(name: "6"),
Test(name: "7"),
Test(name: "8"),
Test(name: "9"),
Test(name: "10"),
Test(name: "12"),
Test(name: "12"),
Test(name: "13"),
Test(name: "14"),
Test(name: "15"),
]

Related

How to add a first responder to a multi line text field

I have a multi line text field, and need it to become a first responder when it appears. Also, it needs to resign as a first responder when the onCommit parameter of MultiLineTextField is fired, or one of 2 buttons are tapped.
As it stands, the keyboard dismisses when it should but immediately reappears when it shouldn't.
I know I could just use the new iOS 16 TextField .axis parameter, but I need to stick with iOS 15.5, hence the long drawn out code below.
import SwiftUI
struct MultilineTextField: View {
private var placeholder: String
private var onCommit: (() -> Void)?
#State private var viewHeight: CGFloat = 40 //start with one line
#State private var shouldShowPlaceholder = false
#Binding private var text: String
private var internalText: Binding<String> {
Binding<String>(get: { self.text } ) {
self.text = $0
self.shouldShowPlaceholder = $0.isEmpty
}
}
var body: some View {
UITextViewWrapper(text: self.internalText, calculatedHeight: $viewHeight, onDone: onCommit)
.frame(minHeight: viewHeight, maxHeight: viewHeight)
.background(placeholderView, alignment: .topLeading)
}
var placeholderView: some View {
Group {
if shouldShowPlaceholder {
Text(placeholder).foregroundColor(.gray)
.padding(.leading, 4)
.padding(.top, 8)
}
}
}
init (_ placeholder: String = "", text: Binding<String>, onCommit: (() -> Void)? = nil) {
self.placeholder = placeholder
self.onCommit = onCommit
self._text = text
self._shouldShowPlaceholder = State<Bool>(initialValue: self.text.isEmpty)
}
}
private struct UITextViewWrapper: UIViewRepresentable {
typealias UIViewType = UITextView
#Binding var text: String
#Binding var calculatedHeight: CGFloat
var onDone: (() -> Void)?
func makeUIView(context: UIViewRepresentableContext<UITextViewWrapper>) -> UITextView {
let textField = UITextView()
textField.delegate = context.coordinator
textField.isEditable = true
textField.font = UIFont.preferredFont(forTextStyle: .body)
textField.isSelectable = true
textField.isUserInteractionEnabled = true
textField.autocorrectionType = .no
textField.isScrollEnabled = false
textField.backgroundColor = UIColor.clear
textField.keyboardType = .asciiCapable
textField.textColor = .systemBlue
textField.textAlignment = .center
if nil != onDone {
textField.returnKeyType = .done
}
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return textField
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<UITextViewWrapper>) {
if uiView.text != self.text {
uiView.text = self.text
}
if uiView.window != nil, !uiView.isFirstResponder {
uiView.becomeFirstResponder()
}
UITextViewWrapper.recalculateHeight(view: uiView, result: $calculatedHeight)
}
private static func recalculateHeight(view: UIView, result: Binding<CGFloat>) {
let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
if result.wrappedValue != newSize.height {
DispatchQueue.main.async {
result.wrappedValue = newSize.height // call in next render cycle.
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text, height: $calculatedHeight, onDone: onDone)
}
final class Coordinator: NSObject, UITextViewDelegate {
var text: Binding<String>
var calculatedHeight: Binding<CGFloat>
var onDone: (() -> Void)?
init(text: Binding<String>, height: Binding<CGFloat>, onDone: (() -> Void)? = nil) {
self.text = text
self.calculatedHeight = height
self.onDone = onDone
}
func textViewDidChange(_ uiView: UITextView) {
text.wrappedValue = uiView.text
UITextViewWrapper.recalculateHeight(view: uiView, result: calculatedHeight)
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if let onDone = self.onDone, text == "\n" {
textView.resignFirstResponder()
onDone()
return false
}
return true
}
}
}
MultilineTextField Usage:
MultilineTextField("", text: Binding<String>(
get: { userAnswer },
set: {
self.userAnswer = $0.allowedCharacters(string: $0)
self.enableHint()
}), onCommit: {
if self.userAnswer.isEmpty {
nativeForeignPlaceholder = NSMutableAttributedString(string: "Tap here to answer...")
} else {
answerDisabled = true
checkAnswer()
}
})
.overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.systemBlue, lineWidth: overlayLineWidth))
.modifier(TextFieldClearButton(text: $userAnswer))
.placeholder(when: userAnswer.isEmpty) {
TextWithAttributedString(attributedString: nativeForeignPlaceholder ?? NSMutableAttributedString())
}
.font(.system(size: fontSize, weight: .regular, design: .rounded))
.lineLimit(2)
.disabled(answerDisabled)
.onAppear {
// key part: delay setting isFocused until after some-internal-iOS setup
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
answerIsFocused = true
}
}
.onTapGesture(perform: tapOnAnswerField)
.frame(dynamicWidth: 675, dynamicHeight: 100, alignment: .center)
.position(x: geometry.frame(in: .local).midX, y: geometry.frame(in: .local).midY + geometry.size.height / 8.5)
func tapOnAnswerField() {
answerIsFocused = true
}

How can I change background color of PageViewController?

How can I change the background color of the area that is white?
I used UIViewControllerRepresentable but I don't know how to change the color of UIViewControllers.
I guess I need to change the background color in the makeUIViewController function?
I don't know much English, I hope I could explain my problem.
OnboardingView:
struct OnboardingView: View {
#State var currentPageIndex = 0
let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
var subviews = [
UIHostingController(rootView: SubView(imageString: "1")),
UIHostingController(rootView: SubView(imageString: "1")),
UIHostingController(rootView: SubView(imageString: "1"))
]
var titles = ["Take some time out", "Conquer personal hindrances", "Create a peaceful mind"]
var captions = ["Take your time out and bring awareness into your everyday life", "Meditating helps you dealing with anxiety and other psychic problems", "Regular medidation sessions creates a peaceful inner mind"]
var body: some View {
VStack(alignment: .leading) {
Group {
Text(titles[currentPageIndex])
.font(.title)
Text(captions[currentPageIndex])
.font(.subheadline)
.frame(width: 300, height: 50, alignment: .center)
.lineLimit(nil)
}
.padding()
PageViewController(currentPageIndex: $currentPageIndex, viewControllers: subviews)
.frame(height: 600)
.background(Color.yellow)
...
}
}
}
PageViewController:
struct PageViewController: UIViewControllerRepresentable {
#Binding var currentPageIndex: Int
var viewControllers: [UIViewController]
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[viewControllers[currentPageIndex]], direction: .forward, animated: true)
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let index = parent.viewControllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return parent.viewControllers.last
}
return parent.viewControllers[index - 1]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let index = parent.viewControllers.firstIndex(of: viewController) else {
return nil
}
if index + 1 == parent.viewControllers.count {
return parent.viewControllers.first
}
return parent.viewControllers[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = parent.viewControllers.firstIndex(of: visibleViewController)
{
parent.currentPageIndex = index
}
}
}
}
You need to set the backgroundColor of your subviews.
You can do it either in OnboardingView or in PageViewController:
func makeUIViewController(context: Context) -> UIPageViewController {
...
// make the subviews transparent
viewControllers.forEach {
$0.view.backgroundColor = .clear
}
...
}

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")
}
}

show camera roll in a collectionView

I know that this question has answered in this site But I tried the code and receive an Error please help me to show my camera roll in a collection view (I know that I have to add photo usage in info.plist please Just focus on my codes thanks!!!)
here is my view controller code
class translateViewController: UIViewController , UINavigationControllerDelegate , UIImagePickerControllerDelegate , UICollectionViewDataSource, UICollectionViewDelegate {
#IBOutlet var myimageView: UIImageView!
#IBAction func importImage(_ sender: Any) {
let image = UIImagePickerController()
image.delegate = self
image.sourceType = UIImagePickerControllerSourceType.photoLibrary
image.allowsEditing = false
self.present(image , animated: true)
{
}
}
#IBOutlet weak var cameraRollCollectionView: UICollectionView!
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let image = info[UIImagePickerControllerOriginalImage] as? UIImage{
myimageView.image = image
}
else {
print("Error")
}
self.dismiss(animated: true, completion: nil)
}
#IBOutlet var translatebackgroundimg: UIImageView!
#IBOutlet var translatefrontimg: UIImageView!
var assetCollection: PHAssetCollection!
var photosAsset: PHFetchResult<AnyObject>!
var assetThumbnailSize: CGSize!
override func viewDidLoad() {
super.viewDidLoad()
let fetchOptions = PHFetchOptions()
let collection:PHFetchResult = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)
if let first_Obj:AnyObject = collection.firstObject{
//found the album
self.assetCollection = first_Obj as! PHAssetCollection
}
let blurEffect = UIBlurEffect(style: UIBlurEffectStyle.light)
let blurView = UIVisualEffectView(effect: blurEffect)
blurView.frame = CGRect(x: self.translatebackgroundimg.frame.origin.x, y: self.translatebackgroundimg.frame.origin.y, width: self.translatebackgroundimg.frame.size.width, height: self.translatebackgroundimg.frame.size.height)
blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.translatebackgroundimg.addSubview(blurView)
// Do any additional setup after loading the view.
translatefrontimg.image = UIImage(named: "Translate.png")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewDidAppear(_ animated: Bool) {
let blurEffect = UIBlurEffect(style: UIBlurEffectStyle.dark)
let blurView = UIVisualEffectView(effect: blurEffect)
blurView.frame = translatebackgroundimg.bounds
translatebackgroundimg.addSubview(blurView)
translatebackgroundimg.frame = self.view.bounds
}
override func viewWillAppear(_ animated: Bool) {
// Get size of the collectionView cell for thumbnail image
if let layout = self.cameraRollCollectionView!.collectionViewLayout as? UICollectionViewFlowLayout{
let cellSize = layout.itemSize
self.assetThumbnailSize = CGSize(width: cellSize.width, height: cellSize.height)
}
//fetch the photos from collection
self.photosAsset = (PHAsset.fetchAssets(in: self.assetCollection, options: nil) as AnyObject!) as! PHFetchResult<AnyObject>!
self.cameraRollCollectionView!.reloadData()
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of items
var count: Int = 0
if(self.photosAsset != nil){
count = self.photosAsset.count
}
return count;
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cameraCell", for: indexPath as IndexPath)
//Modify the cell
let asset: PHAsset = self.photosAsset[indexPath.item] as! PHAsset
PHImageManager.default().requestImage(for: asset, targetSize: self.assetThumbnailSize, contentMode: .aspectFill, options: nil, resultHandler: {(result, info)in
if result != nil {
cameraCell.userImage.image = result
}
})
return cell
}
// MARK: - UICollectionViewDelegateFlowLayout methods
func collectionView(collectinView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat {
return 4
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat {
return 1
}
// UIImagePickerControllerDelegate Methods
func imagePickerControllerDidCancel(_ picker: UIImagePickerController){
picker.dismiss(animated: true, completion: nil)
}
and here is my CollectionViewCell codes
class cameraCell: UICollectionViewCell , UIImagePickerControllerDelegate {
#IBOutlet weak var userImage: UIImageView!
func configurecell(image: UIImage){
userImage.image = image
}
}