Fortunately, the standard callout view for an MKAnnotationView meets our needs - title, subtitle, leftCalloutAccessoryView, and rightCalloutAccessoryView.
Unfortunately, we use custom fonts in our app, and would like to extend those custom fonts to these callouts.
MKAnnotationView provides no standard way to accomplish this.
How can I use a custom font in an MKAnnotation's callout view?
Since I needed the Swift version - here it is.
Also, you have to call setNeedsLayout() on didAddSubview() because otherwise when you deselect and reselect the annotation layoutSubviews() is not called and the callout has its old font.
// elsewhere, in a category on UIView.
// thanks to this answer: http://stackoverflow.com/a/25877372/607876
typealias ViewBlock = (_ view: UIView) -> Bool
extension UIView {
func loopViewHierarchy(block: ViewBlock?) {
if block?(self) ?? true {
for subview in subviews {
subview.loopViewHierarchy(block: block)
}
}
}
}
// then, in your MKAnnotationView subclass
class CustomFontAnnotationView: MKAnnotationView {
override func didAddSubview(_ subview: UIView) {
if isSelected {
setNeedsLayout()
}
}
override func layoutSubviews() {
// MKAnnotationViews only have subviews if they've been selected.
// short-circuit if there's nothing to loop over
if !isSelected {
return
}
loopViewHierarchy { (view: UIView) -> Bool in
if let label = view as? UILabel {
label.font = labelFont
return false
}
return true
}
}
}
If all you need is a custom font, you need to subclass MKAnnotationView, but you don't have to recreate all the behavior that you get for free with a standard MKAnnotationView. It's actually pretty easy.
Subclass MKAnnotationView
Override -layoutSubviews
When an MKAnnotationView is selected, the callout is added as a subview. Therefore, we can recursively loop through our subclass' subviews and find the UILabel we wish to modify.
That's it!
The only drawback with this method is that you can see the callout adjust it's size if your font is smaller or larger than the standard system font it was expecting. It'd be great if all the adjustments were made before being presented to the user.
// elsewhere, in a category on UIView.
// thanks to this answer: http://stackoverflow.com/a/25877372/607876
//
typedef void(^ViewBlock)(UIView *view, BOOL *stop);
#interface UIView (Helpers)
- (void)loopViewHierarchy:(ViewBlock)block;
#end
#implementation UIView (Helpers)
- (void)loopViewHierarchy:(ViewBlock)block {
BOOL stop = false;
if (block) {
block(self, &stop);
}
if (!stop) {
for (UIView* subview in self.subviews) {
[subview loopViewHierarchy:block];
}
}
}
#end
// then, in your MKAnnotationView subclass
//
#implementation CustomFontAnnotationView
- (void)layoutSubviews
{
// MKAnnotationViews only have subviews if they've been selected.
// short-circuit if there's nothing to loop over
if (!self.selected) {
return;
}
[self loopViewHierarchy:^(UIView *view, BOOL *stop) {
if ([view isKindOfClass:[UILabel class]]) {
*stop = true;
((UILabel *)view).font = {custom_font_name};
}
}];
}
#end
I inspected the view tree by first creating my MKAnnotationView subclass and setting a breakpoint in my overridden -layoutSubviews. In the debugger, I then issued po [self recursiveDescription]. Make sure to turn the breakpoint off when your map first loads, because as mentioned up above, MKAnnotationViews don't have any subviews until their selected. Before you make a selection, enable the breakpoint, tap your pin, break, and print out the view tree. You'll see a UILabel at the very bottom of the tree.
Related
I have a container view that contains the view of a UIPageViewController. This is inside a UIViewController and takes up the whole screen. On top of the container view I have a UIView, covering half the screen, which contains a button and some text. I want to forward the touches from this UIView to the UIPageViewController. This is so that the UIPageViewController can still be swiped left/right even if the user is swiping over the UIView. I also want the button to be able to be pressed, therefore can't just set isUserInteractionEnabled to false on the UIView.
How can I do this?
hitTest is the method which determines who should consume the touches/gestures.
So your "UIView, covering half the screen" can subclass from say NoTouchHandlerView like. And then this view will not consume touches. It would pass then to views under it.
class NoTouchHandlerView: UIView
{
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
{
if let hitTestView = super.hitTest(point, with: event), hitTestView !== self {
return hitTestView
}else {
return nil
}
}
}
Objective C version of the accepted answer for lazy guys like me :)
#implementation NoTouchHandlerView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView* hitTestView = [super hitTest:point withEvent:event];
if (hitTestView != nil && hitTestView != self) {
return hitTestView;
}
return nil;
}
#end
I"m wondering how to essentially transform the objective c code below into swift.
This will loop through all the subviews on my desired view, check if they are textfields, and then check if they are empty of not.
for (UIView *view in contentVw.subviews) {
NSLog(#"%#", view);
if ([view isKindOfClass:[UITextField class]]) {
UITextField *textfield = (UITextField *)view;
if (([textfield.text isEqualToString:""])) {
//show error
return;
}
}
}
Here is where i am with swift translation so far:
for view in self.view.subviews as [UIView] {
if view.isKindOfClass(UITextField) {
//...
}
}
Any help would be great!
Update for Swift 2 (and later): As of Swift 2/Xcode 7 this can be simplified.
Due to the Objective-C "lightweight generics", self.view.subviews
is already declared as [UIView] in Swift, therefore the cast
is not necessary anymore.
Enumeration and optional cast can be combined with to a for-loop
with a case-pattern.
This gives:
for case let textField as UITextField in self.view.subviews {
if textField.text == "" {
// show error
return
}
}
Old answer for Swift 1.2:
In Swift this is nicely done with the optional downcast operator as?:
for view in self.view.subviews as! [UIView] {
if let textField = view as? UITextField {
if textField.text == "" {
// show error
return
}
}
}
See "Downcasting"
in the Swift book.
Swift 5 and Swift 4: -
A Very simple answer you can understand easyly : - You can handle all kind of Objects like UILable, UITextfields, UIButtons, UIView, UIImages . any kind of objecs etc.
for subview in self.view.subviews
{
if subview is UITextField
{
//MARK: - if the sub view is UITextField you can handle here
if subview.text == ""
{
//MARK:- Handle your code
}
}
if subview is UIImageView
{
//MARK: - check image
if subview.image == nil
{
//Show or use your code here
}
}
}
//MARK:- You can use it any where, where you need it
//Suppose i need it in didload function we can use it and work it what do you need
override func viewDidLoad() {
super.viewDidLoad()
for subview in self.view.subviews
{
if subview is UITextField
{
//MARK: - if the sub view is UITextField you can handle here
if subview.text == ""
{
//MARK:- Handle your code
}
}
if subview is UIImageView
{
//MARK: - check image
if subview.image == nil
{
//Show or use your code here
}
}
}
}
I have UITableView with hided separator line, and when I dragging cell, shadows appears some kind as borders comes out on up and down. How to hide this? Please see example:
Great thanks!
So, I have answer, just subclass of UITableView with method:
- (void) didAddSubview:(UIView *)subview
{
[super didAddSubview:subview];
if([subview.class.description isEqualToString:#"UIShadowView"]) {
subview.hidden = YES;
}
}
NoShadowTableView.m
#import "NoShadowTableView.h"
#interface NoShadowTableView ()
{
// iOS7
__weak UIView* wrapperView;
}
#end
#implementation NoShadowTableView
- (void) didAddSubview:(UIView *)subview
{
[super didAddSubview:subview];
// iOS7
if(wrapperView == nil && [[[subview class] description] isEqualToString:#"UITableViewWrapperView"])
wrapperView = subview;
// iOS6
if([[[subview class] description] isEqualToString:#"UIShadowView"])
[subview setHidden:YES];
}
- (void) layoutSubviews
{
[super layoutSubviews];
// iOS7
for(UIView* subview in wrapperView.subviews)
{
if([[[subview class] description] isEqualToString:#"UIShadowView"])
[subview setHidden:YES];
}
}
#end
Quick hack is subclassing UITableViewCell and add method:
override func layoutSubviews() {
super.layoutSubviews()
superview?.subviews.filter({ "\(type(of: $0))" == "UIShadowView" }).forEach { (sv: UIView) in
sv.removeFromSuperview()
}
}
This code works for me!
import UIKit
class NoShadowTableView: UITableView {
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
if "\(type(of: subview))" == "UIShadowView" {
subview.removeFromSuperview()
}
}
}
I was facing a similar problem by using default UITableView reordering controls. So I used this external third-party library which solved my problem.
https://github.com/shusta/ReorderingTableViewController
Hope this helps
Swift 3 implementation (removed iOS6 support)
import UIKit
class NoShadowTableView: UITableView {
weak var wrapperView: UIView?
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
if wrapperView == nil && "\(type(of: subview))" == "UITableViewWrapperView" {
wrapperView = subview
}
}
override func layoutSubviews() {
super.layoutSubviews()
wrapperView?.subviews.forEach({ view in
if "\(type(of: view))" == "UIShadowView" {
view.isHidden = true
}
})
}
}
For me hacks with the UIShadowView didn't work. I checked the solution on iOS 10. But one line in a cell class has done the trick:
override func layoutSubviews() {
super.layoutSubviews()
self.subviews.filter{ $0 is UIImageView }.forEach { $0.isHidden = true }
}
Swift 4 solution; use extended uitableviewcontroller because now shadow is in the table and only added when cell is moved.
class UITableViewControllerEx: UITableViewController {
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
view.subviews.filter({ String(describing: type(of: $0)) == "UIShadowView" }).forEach { (sv: UIView) in
sv.isHidden = true
}
}
}
All of the other answers appear to be using private APIs which is obviously not a good thing to do. Additionally, they don't appear to work anymore (at least not on iOS 16).
I found that using UIDragPreviewParameters allowed me to get rid of the horrible background colour and reshape the shadow to fit one of the subviews in my table view cell.
-(UIDragPreviewParameters *)tableView:(UITableView *)tableView dragPreviewParametersForRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
return [self createParametersForTableView:tableView atIndexPath:indexPath];
}
-(UIDragPreviewParameters *)tableView:(UITableView *)tableView dropPreviewParametersForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self createParametersForTableView:tableView atIndexPath:indexPath];
}
-(UIDragPreviewParameters *)createParametersForTableView:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath {
// Get the selected table view cell.
CustomCell *cell = [tableView cellForRowAtIndexPath:indexPath];
// Create the dragged cell preview parameters.
UIDragPreviewParameters *parameters = [[UIDragPreviewParameters alloc] init];
[parameters setBackgroundColor:[UIColor clearColor]];
[parameters setVisiblePath:[UIBezierPath bezierPathWithRoundedRect:cell.customSubView.frame cornerRadius:22.2]];
[parameters setShadowPath:[UIBezierPath bezierPathWithRoundedRect:cell.customSubView.frame cornerRadius:22.2]];
return parameters;
}
If you use this method, you will need to implement two delegates: UITableViewDragDelegate and UITableViewDropDelegate.
[yourTableView setDragDelegate:self];
[yourTableView setDropDelegate:self];
The drag delegate method will allow you to control the preview background when dragging the cell. When dropping the cell, you need to use the drop delegate method to avoid the preview background going back to the default white background.
I just discovered that if I do the following:
Click the button that animates a UIPickerView into my view
Quickly start the wheel rolling towards, then past, the last item
Dismiss the view with a button
Then it has not yet selected the last item yet.
I tried this by simply outputting to the console whenever the didSelectRow method was fired, and it fires when the wheel stabilizes on the last item.
Can I detect that the wheel is still rolling, so that I can delay checking it for a selected value until it stabilizes?
If it matters, I'm programming in MonoTouch, but I can read Objective-C code well enough to reimplement it, if you have a code example that is.
As animation keys don't work, I wrote this simple function that works for detecting if a UIPickerView is currently moving.
-(bool) anySubViewScrolling:(UIView*)view
{
if( [ view isKindOfClass:[ UIScrollView class ] ] )
{
UIScrollView* scroll_view = (UIScrollView*) view;
if( scroll_view.dragging || scroll_view.decelerating )
{
return true;
}
}
for( UIView *sub_view in [ view subviews ] )
{
if( [ self anySubViewScrolling:sub_view ] )
{
return true;
}
}
return false;
}
It ends up returning true five levels deep.
Swift 4 (updated) version with extension of #DrainBoy answers
extension UIView {
func isScrolling () -> Bool {
if let scrollView = self as? UIScrollView {
if (scrollView.isDragging || scrollView.isDecelerating) {
return true
}
}
for subview in self.subviews {
if ( subview.isScrolling() ) {
return true
}
}
return false
}
}
Since animationKeys seems to not work anymore, I have another solution. If you check the subviews of UIPickerView, you'll see that there is a UIPickerTableView for each component.
This UIPickerTableView is indeed a subclass of UITableView and of course of UIScrollView. Therefore, you can check its contentOffset value to detect a difference.
Besides, its scrollViewDelegate is nil by default, so I assume you can safely set an object of yours to detect scrollViewWillBeginDragging, scrollViewDidEndDecelerating, etc.
By keeping a reference to each UIPickerTableView, you should be able to implement an efficient isWheelRolling method.
Expanded #iluvatar_GR answer
extension UIView {
func isScrolling () -> Bool {
if let scrollView = self as? UIScrollView {
if (scrollView.isDragging || scrollView.isDecelerating) {
return true
}
}
for subview in self.subviews {
if ( subview.isScrolling() ) {
return true
}
}
return false
}
func waitTillDoneScrolling (completion: #escaping () -> Void) {
var isMoving = true
DispatchQueue.global(qos: .background).async {
while isMoving == true {
isMoving = self.isScrolling()
}
DispatchQueue.main.async {
completion()}
}
}
}
Expanded #iluvatar_GR, #Robert_at_Nextgensystems answer
Used Gesture, UIScrollView isDragging or isDecelerating.
// Call it every time when Guesture action.
#objc func respondToSwipeGesture(gesture: UIGestureRecognizer) {
// Changes the button name to scrolling at the start of scrolling.
DispatchQueue.main.async {
self._button.setTitle("Scrolling...", for: .normal)
self._button.isEnabled = false
self._button.backgroundColor = Utils.hexStringToUIColor(hex: "FF8FAE")
}
// Indication according to scrolling status
_datePicker.waitTillDoneScrolling(completion: {
print("completion")
DispatchQueue.main.async {
self._button.setTitle("Completion", for: .normal)
self._button.isEnabled = true
self._button.backgroundColor = Utils.hexStringToUIColor(hex: "7CB0FF")
}
})
}
[SWIFT4] Share Example Source link!
enter Sample Source link
Reference : How to recognize swipe in all 4 directions
I think you can just check if the UIPickerView is in the middle of animating and wait for it to stop. This was answered here link
You can use a SwipeGestureRecognizer on the picker.
I assume this is not a perfect solution at all.
- (void)viewDidLoad
{
[super viewDidLoad];
_pickerSwipeGestureRecognizer.delegate = self;
[_pickerSwipeGestureRecognizer setDirection:(UISwipeGestureRecognizerDirectionDown | UISwipeGestureRecognizerDirectionUp)];
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
if([gestureRecognizer isEqual:_pickerSwipeGestureRecognizer]){
NSLog(#"start");
}
}
- (void)pickerView:(UIPickerView *)thePickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
NSLog(#"end");
}
I'm having a hard time getting the UITextView to disable the selecting of the text.
I've tried:
canCancelContentTouches = YES;
I've tried subclassing and overwriting:
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
(But that gets called only After the selection)
- (BOOL)touchesShouldCancelInContentView:(UIView *)view;
(I don't see that getting fired at all)
- (BOOL)touchesShouldBegin:(NSSet *)touches
withEvent:(UIEvent *)event
inContentView:(UIView *)view;
(I don't see that getting fired either)
What am I missing?
Issue How disable Copy, Cut, Select, Select All in UITextView has a workable solution to this that I've just implemented and verified:
Subclass UITextView and overwrite canBecomeFirstResponder:
- (BOOL)canBecomeFirstResponder {
return NO;
}
Note that this disables links and other tappable text content.
I've found that calling
[textView setUserInteractionEnabled:NO];
works quite well.
UITextView's selectable property:
This property controls the ability of the user to select content and
interact with URLs and text attachments. The default value is YES.
Swift 4, Xcode 10
This solution will
disable highlighting
enable tapping links
allow scrolling
Make sure you set the delegate to YourViewController
yourTextView.delegate = yourViewControllerInstance
Then
extension YourViewController: UITextViewDelegate {
func textViewDidChangeSelection(_ textView: UITextView) {
if #available(iOS 13, *) {
textView.selectedTextRange = nil
} else {
view.endEditing(true)
}
}
}
Swift 4, Xcode 10:
If you want to make it so the user isn't able to select or edit the text.
This makes it so it can not be edited:
textView.isEditable = false
This disables all user interaction:
textView.isUserInteractionEnabled = false
This makes it so that you can't select it. Meaning it will not show the edit or paste options. I think this is what you are looking for.
textView.isSelectable = false
It sounds like what you actually want is a giant UILabel inside a UIScrollView, and not a UITextView.
update: if you are on newer versions of iOS UILabel now has a lines property:
Multiple lines of text in UILabel
If you just want to prevent it from being edited, then set the UITextView's "editable" property to NO/False.
If you're trying to leave it editable but not selectable, that's going to be tricky. You might need to create a hidden textview that the user can type into and then have UITextView observe that hidden textview and populate itself with the textview's text.
To do this first subclass the UITextView
and in the implementation do the following
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event
{
self.selectable = NO;
}
- (void)touchesCancelled:(nullable NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event
{
self.selectable = YES;
}
this should work fine,
Did you try setting userInteractionEnabled to NO for your UITextView? But you'd lose scrolling too.
If you need scrolling, which is probably why you used a UITextView and not a UILabel, then you need to do more work. You'll probably have to override canPerformAction:withSender: to return NO for actions that you don't want to allow:
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
switch (action) {
case #selector(paste:):
case #selector(copy:):
case #selector(cut:):
case #selector(cut:):
case #selector(select:):
case #selector(selectAll:):
return NO;
}
return [super canPerformAction:action withSender:sender];
}
For more, UIResponderStandardEditActions .
You can disable text selection by subclassing UITextView.
The below solution is:
compatible with isScrollEnabled
compatible with loupe/magnifier
but not compatible with links (see here for a solution compatible with links)
/// Class to disallow text selection
/// while keeping support for loupe/magnifier and scrolling
/// https://stackoverflow.com/a/49428248/1033581
class UnselectableTextView: UITextView {
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
// prevents selection from loupe/magnifier (_UITextSelectionForceGesture), multi tap, tap and a half, etc.
// without losing the loupe/magnifier or scrolling
// but we lose taps on links
addSubview(transparentOverlayView)
}
let transparentOverlayView: UIView = {
$0.backgroundColor = .clear
$0.autoresizingMask = [.flexibleHeight, .flexibleWidth]
return $0
}(UIView())
override var contentSize: CGSize {
didSet {
transparentOverlayView.frame = CGRect(origin: .zero, size: contentSize)
}
}
// required to prevent blue background selection from any situation
override var selectedTextRange: UITextRange? {
get { return nil }
set {}
}
}
For swift, there is a property called "isSelectable" and its by default assign to true
you can use it as follow:
textView.isSelectable = false