Swift - A way to force redrawing of UIScrollView - ios

I have an UIViewController where there is an UIScrollView, there also is a function (build()) to draw UIView's in the UIScrollView.
Problem : Nothing appears in the UIViewController, but when I scroll (so when I update the UIScrollView) the UIView's, I wanted, appear.
I tried setNeedsDisplay() to force the redrawing but it doesn't work...
A video to help you to understand the problem :
https://vimeo.com/user87689481/review/281597574/3cccb78dc1
Here is the code of the UIViewController:
import UIKit
class MainViewController: UIViewController {
#IBOutlet weak var body: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
navigationController?.navigationBar.prefersLargeTitles = true
navigationController?.navigationItem.largeTitleDisplayMode = .always
DispatchQueue.global(qos: .background).async {
while !Home.accessories.isInitialized {}
DispatchQueue.main.async {
self.build(Home.accessories.toUIView())
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
public func build(_ s: [UISectionView]) {
let topMargin = 10
let rightMargin = 10
let leftMargin = 10
let space = 5
let heightItem = 60
var b = topMargin
for t in s {
if t.isHidden == true {
continue
}
if t.title != nil {
let f = UIFont(name: "Helvetica", size: 20)
let l = UILabel(frame: CGRect(x: rightMargin, y : b, width: Int(UIScreen.main.bounds.width) - (rightMargin + leftMargin), height: Int(f!.lineHeight)))
l.font = f
l.text = t.title
body.addSubview(l)
b = b + Int(f!.lineHeight) + space
}
for i in t.items {
body.addSubview(i.getView(frame: CGRect(x: rightMargin, y: b, width: Int(UIScreen.main.bounds.width) - (rightMargin + leftMargin), height: heightItem), view: self))
b = b + heightItem + space
}
}
body.contentSize = CGSize(width: UIScreen.main.bounds.width, height: CGFloat(b))
//a code to force body (UIScrollView) to refresh/relaod/redraw/update/...
}
}
I hope we can help me =D
Thanks in advance !

There is no method to redraw a UIScrollView, then only thing you need to do is:
Add views to UIScrollView
Set contentSize of the UIScrollView so that it allows user to pan around and see all of the contents of the UIScrollView
What you are doing in your code seems ok, because you are doing both of the things listed above.
Problem is either if your ts are hidden:
if t.isHidden == true {
continue
}
Or you are calling build() method somewhere else on the Background thread
To be sure you are not doing that you can just add assert(Thread.isMainThread, "Never call me on background thread") as the first line in build() method

Related

Blinking cursor as default in Swift

How can I make cursor blink when the view is opened without touching anywhere. I am currently using becomeFirstResponder() My text field becomes really first responder but when the view is opened, blink does not blink. Why?
My code is here.
import UIKit
class ViewController: UIViewController {
let resultTextField: UITextField = {
var myField = UITextField()
myField.textColor = .blue
myField.font = .systemFont(ofSize: 36)
myField.textAlignment = .right
myField.borderStyle = .roundedRect
return myField
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(resultTextField)
resultTextField.becomeFirstResponder()
resultTextField.inputView = UIView()
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
let myWidth = self.view.frame.width
let myHeight = self.view.frame.height
resultTextField.frame = CGRect(x: (myWidth - (myWidth / 1.15))/2,
y: myHeight / 7.5,
width: myWidth / 1.15,
height: myHeight / 15)
}
}
Thanks
I agree with #Luca, try calling becomeFirstResponder in viewDidAppear or in your layoutSubviews function. viewDidLoad()only means that your UIViewController has been loaded to memory but is not displayed yet. Easy way to verify this is implementing a subclass from UITextField and override the caretRect which returns the cursor's frame. Set a breakpoint and test the code ...
class CustomTextField: UITextField {
override func caretRect(for position: UITextPosition) -> CGRect {
/// set a breakpoint and you will notice that it's not called with the viewDidLoad() of your UIViewController
return super.caretRect(for: position)
}
}

How Do I Properly Use iCarouselOptions with iOS iCarousel?

I'm creating an iOS app with Swift and am using iCarousel from GitHub (installed via cocoapods) and I'm not sure what I'm doing wrong, but I can't seem to get iCarouselOptions I set to have any effect...
What I want to do is simply change the spacing of the UIViews in my carousel (They are too close together and overlapping, which I don't want).
Here is my code:
import UIKit
import iCarousel
class MiniGamesViewController: UIViewController, iCarouselDelegate, iCarouselDataSource {
//Objects
#IBOutlet weak var leaveMinigamesButton: UIButton!
//Variables
var carouselHeight : CGFloat!
var selectionBallSize : CGFloat!
override func viewDidLoad() {
super.viewDidLoad()
leaveMinigamesButton.layer.zPosition = 3
carouselHeight = self.view.frame.size.height - 200
selectionBallSize = self.view.frame.size.width * 0.65
let carousel = iCarousel(frame: CGRect(
x: 0,
y: 100,
width: self.view.frame.size.width,
height: carouselHeight))
carousel.perspective = -0.005
carousel.centerItemWhenSelected = true
carousel.decelerationRate = 0.9
carousel.viewpointOffset = CGSize(width: 0, height: 0) //-selectionBallSize * 1.2
carousel.dataSource = self
carousel.type = .rotary
view.addSubview(carousel)
}
override func viewDidAppear(_ animated: Bool) {
setNeedsStatusBarAppearanceUpdate()
}
//This is how many items there will be in my rotary iCarousel
func numberOfItems(in carousel: iCarousel) -> Int {
return 6
}
//This defines each item or 'cell' in the iCarousel
func carousel(_ carousel: iCarousel, viewForItemAt index: Int, reusing view: UIView?) -> UIView {
let imageView: UIImageView
if view != nil {
imageView = view as! UIImageView
} else {
imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: selectionBallSize, height: selectionBallSize))
}
imageView.image = UIImage(named: "orbCyan")
return imageView
}
func carousel(_ carousel: iCarousel, valueFor option: iCarouselOption, withDefault value: CGFloat) -> CGFloat {
switch (option) {
case .spacing: return value * 2
case .radius: return 200
case .fadeMinAlpha: return 0.5
default: return value
}
}
}
Specifically the very bottom of my code is where the customization
is supposed to take effect, but it doesn't seem to be making any changes when I run the app. At the moment, I'm only concerned with trying to space the UIViews in the carousel to be further apart.
Am I putting the iCarouselOptions function in the right place? Is it even being 'called' at all?
Thanks for your help!
Wouldn't you know it, as soon as I posted this question I found the issue haha (after a day of trying everything). Turns out I was missing the simple line:
carousel.delegate = self
underneath:
carousel.dataSource = self
That totally did the trick and it works now lol. I need a fresh set of eyes :P

Swift addSubview() on views created using init(repeating:count) doesn't work

Here is a ViewController that creates 4 subviews using init(repeating:count).
In viewDidLoad I add them as subviews and set their frames. When I run the application only the last view is added.
class ViewController: UIViewController {
let subviews = [UIView].init(repeating: UIView(), count: 4)
override func viewDidLoad() {
super.viewDidLoad()
for i in 0..<subviews.count {
self.view.addSubview(subviews[i])
self.subviews[i].backgroundColor = UIColor.red
self.subviews[i].frame = CGRect(x: CGFloat(i) * 35, y: 30, width: 30, height: 30)
}
}
}
Here's the same code except instead of using init(repeating:count) I use
a closure. This works fine-- all subviews are added.
class ViewController: UIViewController {
let subviews: [UIView] = {
var subviews = [UIView]()
for i in 0..<4 {
subviews.append(UIView())
}
return subviews
}()
override func viewDidLoad() {
//same as above...
}
}
You’ve put the same instance of UIView in your array four times. Your viewDidLoad just ends up moving that single view around. You need to create four separate instances of UIView.
let subviews = (0 ..< 4).map({ _ in UIView() })

Making a UIView update behind another one in the same view controller

I'm working in swift to make an app the uses the cocoapod framework BBSlideoutMenu to display a menu. I am also using the cocoapod framework ChameleonFramework. What I'm trying to do is make the hamburger button that I'm using change colour when it is opened. I haven't yet implemented it, but I also want to make the bar on top transparent. I've recorded it here so you can see what is happening. Basically, the view only gets updated when I slide away the menu.
Disclaimer: I am aware that using a hamburger menu is viewed as bad code design, unfortunately it is what I need in this app.
Here is my code:
import UIKit
import BBSlideoutMenu
import ChameleonFramework
class ViewController: UIViewController, BBSlideoutMenuDelegate {
#IBOutlet var slideMenu: BBSlideoutMenu!
var button: HamburgerButton! = nil
var menuOpen = false;
let screenSize: CGRect = UIScreen.mainScreen().bounds
let topBar = UIView(frame: CGRectMake(0, 0, UIScreen.mainScreen().bounds.width, UIScreen.mainScreen().bounds.height * 0.1))
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib
view.backgroundColor = UIColor.whiteColor()
topBar.backgroundColor = FlatRed()
button = HamburgerButton(frame: CGRectMake(0, 20, 54, 54))
button.addTarget(self, action: #selector(toggleMenu(_:)), forControlEvents:.TouchUpInside)
topBar.addSubview(button)
view.addSubview(topBar)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
slideMenu.setupEdgePan()
slideMenu.slideDirection = .Right
slideMenu.shrinkAmount = 0
slideMenu.slideTravelPercent = 0.87
slideMenu.menuOffset = 0
slideMenu.zoomFactor = 1
slideMenu.springEnabled = false
slideMenu.backgroundColor = FlatRed()
slideMenu.delegate = self
slideMenu.setupEdgePan()
}
func toggleMenu(sender : HamburgerButton!) {
if(menuOpen) {
slideMenu.dismissSlideMenu(animated: true, time: nil)
} else {
slideMenu.presentSlideMenu(true) { () -> Void in
//Runs after menu is presented
}
}
}
func didPresentBBSlideoutMenu(menu: BBSlideoutMenu) {
menuOpen = true
button.changeColor(UIColor.blackColor())
}
func didDismissBBSlideoutMenu(menu: BBSlideoutMenu) {
menuOpen = false
button.changeColor(UIColor.whiteColor())
}
}
I am using this hamburger menu button, which has been created with CoreGraphics and QuartzCore, and have added the following function for change colour.
func changeColor(color: UIColor) {
for layer in [ self.topStroke, self.middleStroke, self.bottomStroke ] {
layer.fillColor = nil
layer.strokeColor = color.CGColor
layer.lineWidth = 4
layer.miterLimit = 4
layer.lineCap = kCALineCapRound
layer.masksToBounds = true
let strokingPath = CGPathCreateCopyByStrokingPath(layer.path, nil, 4, .Round, .Miter, 4)
layer.bounds = CGPathGetPathBoundingBox(strokingPath)
layer.actions = [
"strokeStart": NSNull(),
"strokeEnd": NSNull(),
"transform": NSNull()
]
self.layer.addSublayer(layer)
}
}
Edit: I have tried using setNeedsDisplay on the button, the topBar, and both of them consecutively in the functions toggleMenu, didPresentBBSlideoutMenu and didDismissBBSlideoutMenu and it didn't work
I have also tried calling it on the actual view (self.view)
Try calling setNeedsDisplay the button.

Tap Gesture Recognizer not received in custom UIView embedded in super view

I am trying to create a custom UIView/Scrollview named MyScrollView that contains a few labels (UILabel), and these labels receive tap gestures/events in order to respond to user's selections .
In order to make the tap event work on the UILabels, I make sure they all have userIteractionEnabled = true and I created a delegate as below:
protocol MyScrollViewDelegate {
func labelClicked(recognizer: UITapGestureRecognizer)
}
The custom UIView is being used in ScrollViewController that I created, this ScrollViewController implements the delegate method as well:
import UIKit
import Neon
class ScrollViewController: UIViewController, MyScrollViewDelegate {
var curQuestion: IPQuestion?
var type: QuestionViewType?
var lastClickedLabelTag: Int = 0 //
init(type: QuestionViewType, question: IPQuestion) {
super.init(nibName: nil, bundle: nil)
self.curQuestion = question
self.type = type
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.automaticallyAdjustsScrollViewInsets = false
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func loadView() {
view = MyScrollView(delegate: self, q: curQuestion!)
view.userInteractionEnabled = true
}
}
// implementations for MyScrollViewDelegate
extension ScrollViewController {
func labelTitleArray() -> [String]? {
print("labelTitleArray called in implemented delegate")
return ["Comments", "Answers"]
}
func labelClicked(recognizer: UITapGestureRecognizer) {
print("labelClicked called in implemented delegate")
let controller = parentViewController as? ParentViewController
controller?.labelClicked(recognizer)
lastClickedLabelTag = recognizer.view!.tag
}
}
// MARK: - handle parent's ViewController event
extension QuestionDetailViewController {
func updateActiveLabelsColor(index: Int) {
print("updating active labels color: \(index)")
if let view = view as? MyScrollView {
for label in (view.titleScroll.subviews[0].subviews as? [UILabel])! {
if label.tag == index {
label.transform = CGAffineTransformMakeScale(1.1,1.1)
label.textColor = UIColor.purpleColor()
}
else {
label.transform = CGAffineTransformMakeScale(1,1)
label.textColor = UIColor.blackColor()
}
}
}
}
}
This above ScrollViewController is added, as a child view controller to the parent view controller, and positioned to the top part of the parent's view:
override func viewDidLoad() {
super.viewDidLoad()
self.automaticallyAdjustsScrollViewInsets = false
self.view.backgroundColor = UIColor.whiteColor()
addChildViewController(scrollViewController) // added as a child view controller here
view.addSubview(scrollViewController.view) // here .view is MyScrollView
scrollViewController.view.userInteractionEnabled = true
scrollViewController.view.anchorToEdge(.Top, padding: 0, width: view.frame.size.width, height: 100)
}
The app can load everything up in the view, but the tap gesture/events are not passed down to the labels in the custom MyScrollView. For this, I did some google search and have read Event Delivery: Responder Chain on Apple Developer website and did a hit test as well. The hitTest function below can be triggered in the MyScrollView:
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
print("hit test started, point: \(point), event: \(event)")
return self
}
My observations with the hitTest is that the touchesBegan() and touchesEnded() methods are triggered in the view only when the hitTest function is there. Without hitTest, both functions do not get called with taps.
but no luck getting the UILabel to respond to Tap Gestures. So I am reaching out to experts on SO here. Thanks for helping!
I think I found out the reason why the UILabel did not respond to tapping after much struggle: the .addGestureRecognizer() method to the label was run in the init() method of my custom UIView component, which is wrong, because the view/label may not have been rendered yet. Instead, I moved that code to the lifecycle method layoutSubviews(), and everything started to work well:
var lastLabel: UILabel? = nil
for i in 0..<scrollTitleArr.count {
let label = UILabel()
label.text = scrollTitleArr[i] ?? "nothing"
print("label: \(label.text)")
label.font = UIFont(name: "System", size: 15)
label.textColor = (i == 0) ? MaterialColor.grey.lighten2 : MaterialColor.grey.darken2
label.transform = (i == 0) ? CGAffineTransformMakeScale(1.1, 1.1) : CGAffineTransformMakeScale(0.9, 0.9)
label.sizeToFit()
label.tag = i // for tracking the label by tag number
label.userInteractionEnabled = true
label.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.labelClicked(_:))))
titleContainer.addSubview(label)
if lastLabel == nil {
label.anchorInCorner(.TopLeft, xPad: 0, yPad: 0, width: 85, height: 40)
// label.anchorToEdge(.Left, padding: 2, width: 85, height: 40)
} else {
label.align(.ToTheRightMatchingTop, relativeTo: lastLabel!, padding: labelHorizontalGap, width: 85, height: 40)
}
lastLabel = label
}
In addition, I don't need to implement any of the UIGestureRecognizer delegate methods and I don't need to make the container view or the scroll view userInteractionEnabled. More importantly, when embedding the custom UIView to a superview, I configured its size and set clipsToBounds = true.
I guess I should have read more UIView documentation on the Apple Developer website. Hope this will help someone like me in the future! Thanks to all!
You have to set the property userInteractionEnabled = YES.
For some reason, my simulator was frozen or something when the tap gesture recognizer wasn't working. So, when I restarted the app, then it all worked again. I don't know if this applies here, but that was the fix for me.

Resources