How can I set the width of a Custom PickerView in SwiftUI? - swiftui

No matter what I try, I cannot control the width of a custom picker view integrated into Swift UI with UIViewRepresentable.
Here is the ContentView:
struct ContentView: View {
#State private var pickerData:[[String]] =
[
["silhouette_1" , "silhouette_2" , "silhouette_3" ],
["silhouette_2" , "silhouette_3" , "silhouette_1" ],
["silhouette_3" , "silhouette_1" , "silhouette_2" ]
]
var body: some View {
VStack{
SilhouettePickerView(pickerData: $pickerData)
}
}
}
And here is the SilhouettePickerView:
struct SilhouettePickerView: UIViewRepresentable {
#Binding var pickerData : [[String]]
func makeCoordinator() -> SilhouettePickerView.Coordinator {
return SilhouettePickerView.Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<SilhouettePickerView>) -> UIPickerView {
let picker = UIPickerView(frame: .zero)
//let picker = UIPickerView(frame: CGRect(x: 0, y: 0, width: ????, height: ?????))
picker.dataSource = context.coordinator
picker.delegate = context.coordinator
return picker
}
func updateUIView(_ view: UIPickerView, context: UIViewRepresentableContext<SilhouettePickerView>) {
}
class Coordinator: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
var parent: SilhouettePickerView
init(_ pickerView: SilhouettePickerView) {
self.parent = pickerView
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return parent.pickerData.count
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return parent.pickerData[component].count
}
func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat {
//return UIScreen.main.bounds.width/3
pickerView.bounds.width/3 - 8
}
func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
return 126
}
func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
let view = UIView(frame: CGRect(x: 0, y: 0, width:pickerView.bounds.width, height: 0))
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 90, height: 126))
for _ in 0..<self.parent.pickerData.count {
imageView.image = UIImage(named: self.parent.pickerData[component][row])
}
view.addSubview(imageView)
return view
}
}
}
This is the result.
I want it to fill up more of the screen. Also, I want the component rows centered; you'll notice the gray at the right edge. (Anyway to get rid of that?)
I've played around with setting the frame size in makeUIView:
let picker = UIPickerView(frame: CGRect(x: 0, y: 0, width: 400, height: 200))
But nothing changes.
Any suggestions? Thanks!

Related

How do I make a UIBarButtonItem (inside of a UIToolbar, inside of a UITextView) change background colour/image when selected?

I am trying to make a WYSIWYG editor by interfacing between SwiftUI and UIKit via a UIViewRepresentable.
I am not sure how to change the background colour/image of a UIBarButtonItem to show that the underline attribute button is selected. The UIBarButtonItem is inside of a UIToolbar, which is inside of a UITextView. I need the underline button to be a different colour when either the selected text contains the underline attribute, or if the typingAttributes contains the underline attribute (because the underline button has been selected).
Any help would be greatly appreciated.
Below is the code:
import SwiftUI
import UIKit
struct ContentView: View {
#State private var mutableString: NSMutableAttributedString = NSMutableAttributedString(
string: "this is the NSMutableAttributeString with no attributes")
var body: some View {
WYSIWYG(outerMutableString: $mutableString)
}
}
struct WYSIWYG: UIViewRepresentable {
#Binding var outerMutableString: NSMutableAttributedString
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIView(context: Context) -> UITextView {
context.coordinator.textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.attributedText = outerMutableString
context.coordinator.stringDidChange = { string in
outerMutableString = string
}
}
class Coordinator: NSObject, UITextViewDelegate {
private let fontSize: CGFloat = 32.0
// var to check if the underline button has been pressed
private var underlineIsSelected: Bool = false
lazy var textView: UITextView = {
let textView = UITextView()
textView.font = UIFont(name: "Helvetica", size: fontSize)
textView.delegate = self
// make toolbar
let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: textView.frame.size.width, height: 44))
// make toolbar underline button
let underlineButton = UIBarButtonItem(
image: UIImage(systemName: "underline"),
style: .plain,
target: self,
action: #selector(underline))
toolBar.items = [underlineButton]
toolBar.sizeToFit()
textView.inputAccessoryView = toolBar
return textView
}()
var stringDidChange: ((NSMutableAttributedString) -> ())?
func textViewDidChange(_ textView: UITextView) {
stringDidChange?(textView.textStorage)
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
var list: [NSAttributedString.Key: Any] = [:]
if underlineIsSelected { list[.underlineStyle] = NSUnderlineStyle.single.rawValue }
textView.typingAttributes = list
stringDidChange?(textView.textStorage)
return true
}
func textViewDidChangeSelection(_ textView: UITextView) { }
#objc func underline() {
let range = textView.selectedRange
if (range.length > 0) {
if (isActive(key: .underlineStyle)) {
textView.textStorage.removeAttribute(
.underlineStyle,
range: range)
} else {
textView.textStorage.addAttribute(
.underlineStyle,
value: NSUnderlineStyle.single.rawValue,
range: range)
}
stringDidChange?(textView.textStorage)
}
underlineIsSelected.toggle()
}
// func to check if the selected part of the NSMutableAttributedString contains the attribute key
func isActive(key: NSAttributedString.Key) -> Bool {
var range = textView.selectedRange
if range.length > 0 {
return (textView.textStorage.attribute(
key,
at: range.location,
longestEffectiveRange: &range,
in: range) != nil) ? true : false
}
return false
}
}
}

Using SwiftUI View for UICollectionViewCell

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"),
]

How do I change the height of this UIViewRepresentable?

I found this code on the web :
struct CustomTextField: UIViewRepresentable {
#Binding var text: String
#State var placeholder: String
func makeCoordinator() -> Coordinator {
Coordinator(text: $text)
}
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.borderStyle = .roundedRect
textField.placeholder = placeholder
textField.autocapitalizationType = .none
textField.autocorrectionType = .no
textField.spellCheckingType = .no
textField.keyboardType = .URL
textField.frame = CGRect(x: 0, y: 0, width: 20.00, height: 10)
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ view: UITextField, context: Context) {
view.text = text
}
}
extension CustomTextField {
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
init(text: Binding<String>) {
_text = text
}
func textFieldDidChangeSelection(_ textField: UITextField) {
DispatchQueue.main.async {
self.text = textField.text ?? ""
}
}
}
}
The code works absolutely fine. The problem with this is that I am not able to find a suitable way to increase the height of this. As you can see, I tried to use a CGRect as the frame, to no effect. How can I change the size (particularly height in my specific scenario) of this custom UIViewRepresentable?
Just the same you would do with any other SwiftUI view:
CustomTextField(text: $text, placeholder: "")
// constant
.frame(height: 100)
// fill available height
.frame(maxHeight: .infinity)
If you wanna make it respect intrinsicContentSize, check out this answer

Toolbar accessory added to UITextView as a UIViewRepresentable only displays after first launch of iMessage extension application

Would like to have the toolbar show all the time.
Any help is greatly appreciated as this is a real drag for the user experience.
I've added a toolbar to the keyboard for the TextView as shown below.
However the toolbar only shows after the app has run once. Meaning the toolbar does not show the first time the app is run. the app works every time after the initial load.
This is on IOS 14.3, Xcode 12.3, Swift 5, iMessage extension app. Fails on simulator or real device.
struct CustomTextEditor: UIViewRepresentable {
#Binding var text: String
private var returnType: UIReturnKeyType
private var keyType: UIKeyboardType
private var displayDoneBar: Bool
private var commitHandler: (()->Void)?
init(text: Binding<String>,
returnType: UIReturnKeyType = .done,
keyboardType: UIKeyboardType,
displayDoneBar: Bool,
onCommit: (()->Void)?) {
self._text = text
self.returnType = returnType
self.keyType = keyboardType
self.displayDoneBar = displayDoneBar
self.commitHandler = onCommit
}
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.keyboardType = keyType
textView.returnKeyType = returnType
textView.backgroundColor = .clear
textView.font = UIFont.systemFont(ofSize: 20, weight: .regular)
textView.isEditable = true
textView.delegate = context.coordinator
if self.displayDoneBar {
let flexibleSpace = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace,
target: self,
action: nil)
let doneButton = UIBarButtonItem(title: "Close Keyboard",
style: .done,
target: self,
action: #selector(textView.doneButtonPressed(button:)))
let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 300, height: 50))
toolBar.items = [flexibleSpace, doneButton, flexibleSpace]
toolBar.setItems([flexibleSpace, doneButton, flexibleSpace], animated: true)
toolBar.sizeToFit()
textView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
textView.translatesAutoresizingMaskIntoConstraints = true
textView.inputAccessoryView = toolBar
}
return textView
}
func updateUIView(_ textView: UITextView, context: Context) {
textView.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: CustomTextEditor
init(_ textView: CustomTextEditor) {
self.parent = textView
}
func textViewDidChange(_ textView: UITextView) {
self.parent.$text.wrappedValue = textView.text
}
func textViewDidEndEditing(_ textView: UITextView) {
self.parent.$text.wrappedValue = textView.text
parent.commitHandler?()
}
}
}
extension UITextView {
#objc func doneButtonPressed(button:UIBarButtonItem) -> Void {
self.resignFirstResponder()
}
}
This is how it's called...
import SwiftUI
final class ContentViewHostController: UIHostingController<ContentView> {
weak var myWindow: UIWindow?
init() {
super.init(rootView: ContentView())
}
required init?(coder: NSCoder) {
super.init(coder: coder, rootView: ContentView())
}
}
let kTextColor = Color(hex: "3E484F")
let kOverlayRadius: CGFloat = 10
let kOverlayWidth: CGFloat = 2
let kOverlayColor = kTextColor
struct ContentView: View {
#State var text = ""
var body: some View {
VStack {
Spacer()
CustomTextEditor(text: $text, returnType: .default, keyboardType: .default, displayDoneBar: true, onCommit: nil)
.foregroundColor(kTextColor)
.overlay(
RoundedRectangle(cornerRadius: kOverlayRadius)
.stroke(kOverlayColor, lineWidth: kOverlayWidth)
)
.frame(width: 200, height: 100, alignment: .center)
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
From MessagesViewController...
override func willBecomeActive(with conversation: MSConversation) {
let childViewCtrl = ContentViewHostController()
childViewCtrl.view.layoutIfNeeded() // avoids snapshot warning?
if let window = self.view.window {
childViewCtrl.myWindow = window
window.rootViewController = childViewCtrl
}
}

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
}
...
}