Initial commit.

This commit is contained in:
2019-07-01 14:33:21 +02:00
parent 92a04d779e
commit baa2e0279d
1624 changed files with 3204958 additions and 0 deletions
@@ -0,0 +1,5 @@
#pragma once
void ShowActivityIndicator(UIView* parent, int style);
void ShowActivityIndicator(UIView* parent);
void HideActivityIndicator();
@@ -0,0 +1,66 @@
#include "ActivityIndicator.h"
#include "OrientationSupport.h"
@interface ActivityIndicator : UIActivityIndicatorView
{
UIView* _parent;
}
@end
static ActivityIndicator* _activityIndicator = nil;
@implementation ActivityIndicator
- (void)show:(UIView*)parent
{
_parent = parent;
[parent addSubview: self];
[self startAnimating];
}
- (void)layoutSubviews
{
self.center = CGPointMake([_parent bounds].size.width / 2, [_parent bounds].size.height / 2);
}
@end
void ShowActivityIndicator(UIView* parent, int style)
{
if (_activityIndicator != nil)
return;
if (style >= 0)
{
_activityIndicator = [[ActivityIndicator alloc] initWithActivityIndicatorStyle: (UIActivityIndicatorViewStyle)style];
_activityIndicator.contentScaleFactor = [UIScreen mainScreen].scale;
}
if (_activityIndicator != nil)
[_activityIndicator show: parent];
}
void ShowActivityIndicator(UIView* parent)
{
ShowActivityIndicator(parent, UnityGetShowActivityIndicatorOnLoading());
}
void HideActivityIndicator()
{
if (_activityIndicator)
{
[_activityIndicator stopAnimating];
[_activityIndicator removeFromSuperview];
_activityIndicator = nil;
}
}
extern "C" void UnityStartActivityIndicator()
{
// AppleTV does not support activity indicators
ShowActivityIndicator(UnityGetGLView());
}
extern "C" void UnityStopActivityIndicator()
{
HideActivityIndicator();
}
@@ -0,0 +1,58 @@
#pragma once
typedef struct
{
const char* text;
const char* placeholder;
UIKeyboardType keyboardType;
UITextAutocorrectionType autocorrectionType;
UIKeyboardAppearance appearance;
BOOL multiline;
BOOL secure;
int characterLimit;
}
KeyboardShowParam;
@interface KeyboardDelegate : NSObject<UITextFieldDelegate, UITextViewDelegate>
{
}
- (BOOL)textFieldShouldReturn:(UITextField*)textField;
- (void)textInputDone:(id)sender;
- (void)textInputCancel:(id)sender;
- (void)textInputLostFocus;
- (void)keyboardWillShow:(NSNotification*)notification;
- (void)keyboardDidShow:(NSNotification*)notification;
- (void)keyboardWillHide:(NSNotification*)notification;
// on older devices initial keyboard creation might be slow, so it is good to init in on initial loading.
// on the other hand, if you dont use keyboard (or use it rarely), you can avoid having all related stuff in memory:
// keyboard will be created on demand anyway (in Instance method)
+ (void)Initialize;
+ (KeyboardDelegate*)Instance;
- (id)init;
- (void)setKeyboardParams:(KeyboardShowParam)param;
- (void)show;
- (void)hide;
- (void)positionInput:(CGRect)keyboardRect x:(float)x y:(float)y;
- (void)shouldHideInput:(BOOL)hide;
+ (void)StartReorientation;
+ (void)FinishReorientation;
- (CGRect)queryArea;
- (NSString*)getText;
- (void)setText:(NSString*)newText;
@property (readonly, nonatomic, getter = queryArea) CGRect area;
@property (readonly, nonatomic) BOOL active;
@property (readonly, nonatomic) KeyboardStatus status;
@property (retain, nonatomic, getter = getText, setter = setText:) NSString* text;
@property (assign, nonatomic) int characterLimit;
@property (readonly, nonatomic) BOOL canGetSelection;
@property (nonatomic, getter = querySelection, setter = assignSelection:) NSRange selection;
@end
@@ -0,0 +1,861 @@
#include "Keyboard.h"
#include "DisplayManager.h"
#include "UnityAppController.h"
#include "UnityForwardDecls.h"
#include <string>
#ifndef FILTER_EMOJIS_IOS_KEYBOARD
#define FILTER_EMOJIS_IOS_KEYBOARD 0
#endif
static KeyboardDelegate* _keyboard = nil;
static bool _shouldHideInput = false;
static bool _shouldHideInputChanged = false;
static const unsigned kToolBarHeight = 40;
static const unsigned kSystemButtonsSpace = 2 * 60 + 3 * 18; // empirical value, there is no way to know the exact widths of the system bar buttons
@implementation KeyboardDelegate
{
// UI handling
// in case of single line we use UITextField inside UIToolbar
// in case of multi-line input we use UITextView with UIToolbar as accessory view
// toolbar buttons are kept around to prevent releasing them
// tvOS does not support multiline input thus only UITextField option is implemented
#if PLATFORM_IOS
UITextView* textView;
UIToolbar* viewToolbar;
NSArray* viewToolbarItems;
NSLayoutConstraint* widthConstraint;
#endif
UITextField* textField;
// keep toolbar items for both single- and multi- line edit in NSArray to make sure they are kept around
#if PLATFORM_IOS
UIToolbar* fieldToolbar;
NSArray* fieldToolbarItems;
#endif
// inputView is view used for actual input (it will be responder): UITextField [single-line] or UITextView [multi-line]
// editView is the "root" view for keyboard: UIToolbar [single-line] or UITextView [multi-line]
UIView* inputView;
UIView* editView;
KeyboardShowParam cachedKeyboardParam;
CGRect _area;
NSString* initialText;
UIKeyboardType keyboardType;
BOOL _multiline;
BOOL _inputHidden;
BOOL _active;
KeyboardStatus _status;
int _characterLimit;
// not pretty but seems like easiest way to keep "we are rotating" status
BOOL _rotating;
}
@synthesize area;
@synthesize active = _active;
@synthesize status = _status;
@synthesize text;
@synthesize selection;
- (BOOL)textFieldShouldReturn:(UITextField*)textFieldObj
{
[self textInputDone: nil];
return YES;
}
- (void)textInputDone:(id)sender
{
if (_status == Visible)
_status = Done;
[self hide];
}
- (void)textInputCancel:(id)sender
{
_status = Canceled;
[self hide];
}
- (void)textInputLostFocus
{
if (_status == Visible)
_status = LostFocus;
[self hide];
}
- (BOOL)textViewShouldBeginEditing:(UITextView*)view
{
#if !PLATFORM_TVOS
view.inputAccessoryView = viewToolbar;
#endif
return YES;
}
#if PLATFORM_IOS
- (void)keyboardWillShow:(NSNotification *)notification
{
if (notification.userInfo == nil || inputView == nil)
return;
CGRect srcRect = [[notification.userInfo objectForKey: UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGRect rect = [UnityGetGLView() convertRect: srcRect fromView: nil];
rect.origin.y = [UnityGetGLView() frame].size.height - rect.size.height; // iPhone X sometimes reports wrong y value for keyboard
[self positionInput: rect x: rect.origin.x y: rect.origin.y];
}
- (void)keyboardDidShow:(NSNotification*)notification
{
_active = YES;
}
- (void)keyboardWillHide:(NSNotification*)notification
{
[self systemHideKeyboard];
}
- (void)keyboardDidChangeFrame:(NSNotification*)notification
{
_active = true;
CGRect srcRect = [[notification.userInfo objectForKey: UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGRect rect = [UnityGetGLView() convertRect: srcRect fromView: nil];
if (rect.origin.y >= [UnityGetGLView() bounds].size.height)
[self systemHideKeyboard];
else
{
rect.origin.y = [UnityGetGLView() frame].size.height - rect.size.height; // iPhone X sometimes reports wrong y value for keyboard
[self positionInput: rect x: rect.origin.x y: rect.origin.y];
}
}
#endif
+ (void)Initialize
{
NSAssert(_keyboard == nil, @"[KeyboardDelegate Initialize] called after creating keyboard");
if (!_keyboard)
_keyboard = [[KeyboardDelegate alloc] init];
}
+ (KeyboardDelegate*)Instance
{
if (!_keyboard)
_keyboard = [[KeyboardDelegate alloc] init];
return _keyboard;
}
#if PLATFORM_IOS
struct CreateToolbarResult
{
UIToolbar* toolbar;
NSArray* items;
};
- (CreateToolbarResult)createToolbarWithView:(UIView*)view
{
UIToolbar* toolbar = [[UIToolbar alloc] initWithFrame: CGRectMake(0, 840, 320, kToolBarHeight)];
UnitySetViewTouchProcessing(toolbar, touchesIgnored);
toolbar.hidden = NO;
UIBarButtonItem* inputItem = view ? [[UIBarButtonItem alloc] initWithCustomView: view] : nil;
UIBarButtonItem* doneItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem: UIBarButtonSystemItemDone target: self action: @selector(textInputDone:)];
UIBarButtonItem* cancelItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem: UIBarButtonSystemItemCancel target: self action: @selector(textInputCancel:)];
NSArray* items = view ? @[inputItem, doneItem, cancelItem] : @[doneItem, cancelItem];
toolbar.items = items;
inputItem = nil;
doneItem = nil;
cancelItem = nil;
CreateToolbarResult ret = {toolbar, items};
return ret;
}
#endif
- (id)init
{
NSAssert(_keyboard == nil, @"You can have only one instance of KeyboardDelegate");
self = [super init];
if (self)
{
#if PLATFORM_IOS
textView = [[UITextView alloc] initWithFrame: CGRectMake(0, 840, 480, 30)];
textView.delegate = self;
textView.font = [UIFont systemFontOfSize: 18.0];
textView.hidden = YES;
#endif
textField = [[UITextField alloc] initWithFrame: CGRectMake(0, 0, 120, 30)];
textField.delegate = self;
textField.borderStyle = UITextBorderStyleRoundedRect;
textField.font = [UIFont systemFontOfSize: 20.0];
textField.clearButtonMode = UITextFieldViewModeWhileEditing;
#if PLATFORM_IOS
widthConstraint = [NSLayoutConstraint constraintWithItem: textField attribute: NSLayoutAttributeWidth relatedBy: NSLayoutRelationEqual toItem: nil attribute: NSLayoutAttributeNotAnAttribute multiplier: 1.0 constant: textField.frame.size.width];
[textField addConstraint: widthConstraint];
#endif
#define CREATE_TOOLBAR(t, i, v) \
do { \
CreateToolbarResult res = [self createToolbarWithView:v]; \
t = res.toolbar; \
i = res.items; \
} while(0)
#if PLATFORM_IOS
CREATE_TOOLBAR(viewToolbar, viewToolbarItems, nil);
CREATE_TOOLBAR(fieldToolbar, fieldToolbarItems, textField);
#endif
#undef CREATE_TOOLBAR
#if PLATFORM_IOS
[[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(keyboardWillShow:) name: UIKeyboardWillShowNotification object: nil];
[[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(keyboardDidShow:) name: UIKeyboardDidShowNotification object: nil];
[[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(keyboardWillHide:) name: UIKeyboardWillHideNotification object: nil];
[[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(keyboardDidChangeFrame:) name: UIKeyboardDidChangeFrameNotification object: nil];
#endif
[[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(textInputDone:) name: UITextFieldTextDidEndEditingNotification object: nil];
}
return self;
}
- (void)setTextInputTraits:(id<UITextInputTraits>)traits
withParam:(KeyboardShowParam)param
withCap:(UITextAutocapitalizationType)capitalization
{
traits.keyboardType = param.keyboardType;
traits.autocorrectionType = param.autocorrectionType;
traits.secureTextEntry = param.secure;
traits.keyboardAppearance = param.appearance;
traits.autocapitalizationType = capitalization;
}
- (void)setKeyboardParams:(KeyboardShowParam)param
{
if (!editView.hidden)
{
[NSObject cancelPreviousPerformRequestsWithTarget: self];
if (cachedKeyboardParam.multiline != param.multiline ||
cachedKeyboardParam.secure != param.secure ||
cachedKeyboardParam.keyboardType != param.keyboardType ||
cachedKeyboardParam.autocorrectionType != param.autocorrectionType ||
cachedKeyboardParam.appearance != param.appearance)
{
[self hideUIDelayed];
}
}
cachedKeyboardParam = param;
if (_active)
[self hide];
initialText = param.text ? [[NSString alloc] initWithUTF8String: param.text] : @"";
_characterLimit = param.characterLimit;
UITextAutocapitalizationType capitalization = UITextAutocapitalizationTypeSentences;
if (param.keyboardType == UIKeyboardTypeURL || param.keyboardType == UIKeyboardTypeEmailAddress || param.keyboardType == UIKeyboardTypeWebSearch)
capitalization = UITextAutocapitalizationTypeNone;
#if PLATFORM_IOS
_multiline = param.multiline;
if (_multiline)
{
textView.text = initialText;
[self setTextInputTraits: textView withParam: param withCap: capitalization];
UITextPosition* end = [textView endOfDocument];
UITextRange* endTextRange = [textView textRangeFromPosition: end toPosition: end];
[textView setSelectedTextRange: endTextRange];
}
else
{
textField.text = initialText;
[self setTextInputTraits: textField withParam: param withCap: capitalization];
textField.placeholder = [NSString stringWithUTF8String: param.placeholder];
UITextPosition* end = [textField endOfDocument];
UITextRange* endTextRange = [textField textRangeFromPosition: end toPosition: end];
[textField setSelectedTextRange: endTextRange];
}
inputView = _multiline ? textView : textField;
editView = _multiline ? textView : fieldToolbar;
#else // PLATFORM_TVOS
textField.text = initialText;
[self setTextInputTraits: textField withParam: param withCap: capitalization];
textField.placeholder = [NSString stringWithUTF8String: param.placeholder];
inputView = textField;
editView = textField;
UITextPosition* end = [textField endOfDocument];
UITextRange* endTextRange = [textField textRangeFromPosition: end toPosition: end];
[textField setSelectedTextRange: endTextRange];
#endif
[self shouldHideInput: _shouldHideInput];
_status = Visible;
_active = YES;
}
// we need to show/hide keyboard to react to orientation too, so extract we extract UI fiddling
- (void)showUI
{
// if we unhide everything now the input will be shown smaller then needed quickly (and resized later)
// so unhide only when keyboard is actually shown (we will update it when reacting to ios notifications)
[NSObject cancelPreviousPerformRequestsWithTarget: self];
if (!inputView.isFirstResponder)
{
editView.hidden = YES;
[UnityGetGLView() addSubview: editView];
[inputView becomeFirstResponder];
}
}
- (void)hideUI
{
[NSObject cancelPreviousPerformRequestsWithTarget: self];
[self performSelector: @selector(hideUIDelayed) withObject: nil afterDelay: 0.05]; // to avoid unnecessary hiding
}
- (void)hideUIDelayed
{
[inputView resignFirstResponder];
[editView removeFromSuperview];
editView.hidden = YES;
}
- (void)systemHideKeyboard
{
// when we are rotating os will bombard us with keyboardWillHide: and keyboardDidChangeFrame:
// ignore all of them (we do it here only to simplify code: we call systemHideKeyboard only from these notification handlers)
if (_rotating)
return;
_active = editView.isFirstResponder;
editView.hidden = YES;
_area = CGRectMake(0, 0, 0, 0);
}
- (void)show
{
[self showUI];
}
- (void)hide
{
[self hideUI];
}
- (void)updateInputHidden
{
if (_shouldHideInputChanged)
{
[self shouldHideInput: _shouldHideInput];
_shouldHideInputChanged = false;
}
textField.returnKeyType = _inputHidden ? UIReturnKeyDone : UIReturnKeyDefault;
editView.hidden = _inputHidden ? YES : NO;
inputView.hidden = _inputHidden ? YES : NO;
}
#if PLATFORM_IOS
- (void)positionInput:(CGRect)kbRect x:(float)x y:(float)y
{
float safeAreaInsetLeft = 0;
float safeAreaInsetRight = 0;
#if UNITY_HAS_IOSSDK_11_0
if (@available(iOS 11.0, *))
{
safeAreaInsetLeft = [UnityGetGLView() safeAreaInsets].left;
safeAreaInsetRight = [UnityGetGLView() safeAreaInsets].right;
}
#endif
if (_multiline)
{
// use smaller area for iphones and bigger one for ipads
int height = UnityDeviceDPI() > 300 ? 75 : 100;
editView.frame = CGRectMake(safeAreaInsetLeft, y - height, kbRect.size.width - safeAreaInsetLeft - safeAreaInsetRight, height);
}
else
{
editView.frame = CGRectMake(0, y - kToolBarHeight, kbRect.size.width, kToolBarHeight);
// old constraint must be removed, changing value while constraint is active causes conflict when changing inputView.frame
[inputView removeConstraint: widthConstraint];
inputView.frame = CGRectMake(inputView.frame.origin.x,
inputView.frame.origin.y,
kbRect.size.width - safeAreaInsetLeft - safeAreaInsetRight - kSystemButtonsSpace,
inputView.frame.size.height);
// required to avoid auto-resizing on iOS 11 in case if input text is too long
widthConstraint.constant = inputView.frame.size.width;
[inputView addConstraint: widthConstraint];
}
_area = CGRectMake(x, y, kbRect.size.width, kbRect.size.height);
[self updateInputHidden];
}
#endif
- (CGRect)queryArea
{
return editView.hidden ? _area : CGRectUnion(_area, editView.frame);
}
- (NSRange)querySelection
{
UIView<UITextInput>* textInput;
#if PLATFORM_TVOS
textInput = textField;
#else
textInput = _multiline ? textView : textField;
#endif
UITextPosition* beginning = textInput.beginningOfDocument;
UITextRange* selectedRange = textInput.selectedTextRange;
UITextPosition* selectionStart = selectedRange.start;
UITextPosition* selectionEnd = selectedRange.end;
const NSInteger location = [textInput offsetFromPosition: beginning toPosition: selectionStart];
const NSInteger length = [textInput offsetFromPosition: selectionStart toPosition: selectionEnd];
return NSMakeRange(location, length);
}
- (void)assignSelection:(NSRange)range
{
UIView<UITextInput>* textInput;
#if PLATFORM_TVOS
textInput = textField;
#else
textInput = _multiline ? textView : textField;
#endif
UITextPosition* begin = [textInput beginningOfDocument];
UITextPosition* caret = [textInput positionFromPosition: begin offset: range.location];
UITextPosition* select = [textInput positionFromPosition: caret offset: range.length];
UITextRange* textRange = [textInput textRangeFromPosition: caret toPosition: select];
[textInput setSelectedTextRange: textRange];
}
+ (void)StartReorientation
{
if (_keyboard && _keyboard.active)
_keyboard->_rotating = YES;
}
+ (void)FinishReorientation
{
if (_keyboard)
_keyboard->_rotating = NO;
}
- (NSString*)getText
{
if (_status == Canceled)
return initialText;
else
{
#if PLATFORM_TVOS
return [textField text];
#else
return _multiline ? [textView text] : [textField text];
#endif
}
}
- (void)setText:(NSString*)newText
{
#if PLATFORM_IOS
if (_multiline)
textView.text = newText;
else
textField.text = newText;
#else
textField.text = newText;
#endif
}
- (void)shouldHideInput:(BOOL)hide
{
if (hide)
{
switch (keyboardType)
{
case UIKeyboardTypeDefault: hide = YES; break;
case UIKeyboardTypeASCIICapable: hide = YES; break;
case UIKeyboardTypeNumbersAndPunctuation: hide = YES; break;
case UIKeyboardTypeURL: hide = YES; break;
case UIKeyboardTypeNumberPad: hide = NO; break;
case UIKeyboardTypePhonePad: hide = NO; break;
case UIKeyboardTypeNamePhonePad: hide = NO; break;
case UIKeyboardTypeEmailAddress: hide = YES; break;
case UIKeyboardTypeTwitter: hide = YES; break;
case UIKeyboardTypeWebSearch: hide = YES; break;
default: hide = NO; break;
}
}
_inputHidden = hide;
}
#if FILTER_EMOJIS_IOS_KEYBOARD
static bool StringContainsEmoji(NSString *string);
- (BOOL)textField:(UITextField*)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString*)string_
{
if (range.length + range.location > textField.text.length)
return NO;
return [self currentText: textField.text shouldChangeInRange: range replacementText: string_] && !StringContainsEmoji(string_);
}
- (BOOL)textView:(UITextView*)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString*)text_
{
if (range.length + range.location > textView.text.length)
return NO;
return [self currentText: textView.text shouldChangeInRange: range replacementText: text_] && !StringContainsEmoji(text_);
}
#else
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString*)string_
{
if (range.length + range.location > textField.text.length)
return NO;
return [self currentText: textField.text shouldChangeInRange: range replacementText: string_];
}
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString*)text_
{
if (range.length + range.location > textView.text.length)
return NO;
return [self currentText: textView.text shouldChangeInRange: range replacementText: text_];
}
#endif // FILTER_EMOJIS_IOS_KEYBOARD
- (BOOL)currentText:(NSString*)currentText shouldChangeInRange:(NSRange)range replacementText:(NSString*)text_
{
NSUInteger newLength = currentText.length + (text_.length - range.length);
if (newLength > _characterLimit && _characterLimit != 0 && newLength >= currentText.length)
{
NSString* newReplacementText = @"";
if ((currentText.length - range.length) < _characterLimit)
newReplacementText = [text_ substringWithRange: NSMakeRange(0, _characterLimit - (currentText.length - range.length))];
NSString* newText = [currentText stringByReplacingCharactersInRange: range withString: newReplacementText];
#if PLATFORM_IOS
if (_multiline)
[textView setText: newText];
else
[textField setText: newText];
#else
[textField setText: newText];
#endif
return NO;
}
else
{
return YES;
}
}
@end
//==============================================================================
//
// Unity Interface:
extern "C" void UnityKeyboard_Create(unsigned keyboardType, int autocorrection, int multiline, int secure, int alert, const char* text, const char* placeholder, int characterLimit)
{
#if PLATFORM_TVOS
// Not supported. The API for showing keyboard for editing multi-line text
// is not available on tvOS
multiline = false;
#endif
static const UIKeyboardType keyboardTypes[] =
{
UIKeyboardTypeDefault,
UIKeyboardTypeASCIICapable,
UIKeyboardTypeNumbersAndPunctuation,
UIKeyboardTypeURL,
UIKeyboardTypeNumberPad,
UIKeyboardTypePhonePad,
UIKeyboardTypeNamePhonePad,
UIKeyboardTypeEmailAddress,
UIKeyboardTypeDefault, // Default is used in case Wii U specific NintendoNetworkAccount type is selected (indexed at 8 in UnityEngine.TouchScreenKeyboardType)
UIKeyboardTypeTwitter,
UIKeyboardTypeWebSearch
};
static const UITextAutocorrectionType autocorrectionTypes[] =
{
UITextAutocorrectionTypeNo,
UITextAutocorrectionTypeDefault,
};
static const UIKeyboardAppearance keyboardAppearances[] =
{
UIKeyboardAppearanceDefault,
UIKeyboardAppearanceAlert,
};
KeyboardShowParam param =
{
text, placeholder,
keyboardTypes[keyboardType],
autocorrectionTypes[autocorrection],
keyboardAppearances[alert],
(BOOL)multiline, (BOOL)secure,
characterLimit
};
[[KeyboardDelegate Instance] setKeyboardParams: param];
}
extern "C" void UnityKeyboard_Show()
{
// do not send hide if didnt create keyboard
// TODO: probably assert?
if (!_keyboard)
return;
[[KeyboardDelegate Instance] show];
}
extern "C" void UnityKeyboard_Hide()
{
// do not send hide if didnt create keyboard
// TODO: probably assert?
if (!_keyboard)
return;
[[KeyboardDelegate Instance] textInputLostFocus];
}
extern "C" void UnityKeyboard_SetText(const char* text)
{
[KeyboardDelegate Instance].text = [NSString stringWithUTF8String: text];
}
extern "C" NSString* UnityKeyboard_GetText()
{
return [KeyboardDelegate Instance].text;
}
extern "C" int UnityKeyboard_IsActive()
{
return (_keyboard && _keyboard.active) ? 1 : 0;
}
extern "C" int UnityKeyboard_Status()
{
return _keyboard ? _keyboard.status : Canceled;
}
extern "C" void UnityKeyboard_SetInputHidden(int hidden)
{
_shouldHideInput = hidden;
_shouldHideInputChanged = true;
// update hidden status only if keyboard is on screen to avoid showing input view out of nowhere
if (_keyboard && _keyboard.active)
[_keyboard updateInputHidden];
}
extern "C" int UnityKeyboard_IsInputHidden()
{
return _shouldHideInput ? 1 : 0;
}
extern "C" void UnityKeyboard_GetRect(float* x, float* y, float* w, float* h)
{
CGRect area = _keyboard ? _keyboard.area : CGRectMake(0, 0, 0, 0);
// convert to unity coord system
float multX = (float)GetMainDisplaySurface()->targetW / UnityGetGLView().bounds.size.width;
float multY = (float)GetMainDisplaySurface()->targetH / UnityGetGLView().bounds.size.height;
*x = 0;
*y = area.origin.y * multY;
*w = area.size.width * multX;
*h = area.size.height * multY;
}
extern "C" void UnityKeyboard_SetCharacterLimit(unsigned characterLimit)
{
[KeyboardDelegate Instance].characterLimit = characterLimit;
}
extern "C" int UnityKeyboard_CanGetSelection()
{
return (_keyboard) ? 1 : 0;
}
extern "C" void UnityKeyboard_GetSelection(int* location, int* length)
{
if (_keyboard)
{
NSRange selection = _keyboard.selection;
*location = (int)selection.location;
*length = (int)selection.length;
}
else
{
*location = 0;
*length = 0;
}
}
extern "C" int UnityKeyboard_CanSetSelection()
{
return (_keyboard) ? 1 : 0;
}
extern "C" void UnityKeyboard_SetSelection(int location, int length)
{
if (_keyboard)
{
NSRange range = NSMakeRange(location, length);
_keyboard.selection = range;
}
}
//==============================================================================
//
// Emoji Filtering: unicode magic
#if FILTER_EMOJIS_IOS_KEYBOARD
static bool StringContainsEmoji(NSString *string)
{
__block BOOL returnValue = NO;
[string enumerateSubstringsInRange: NSMakeRange(0, string.length)
options: NSStringEnumerationByComposedCharacterSequences
usingBlock:^(NSString* substring, NSRange substringRange, NSRange enclosingRange, BOOL* stop)
{
const unichar hs = [substring characterAtIndex: 0];
const unichar ls = substring.length > 1 ? [substring characterAtIndex: 1] : 0;
#define IS_IN(val, min, max) (((val) >= (min)) && ((val) <= (max)))
if (IS_IN(hs, 0xD800, 0xDBFF))
{
if (substring.length > 1)
{
const int uc = ((hs - 0xD800) * 0x400) + (ls - 0xDC00) + 0x10000;
// Musical: [U+1D000, U+1D24F]
// Enclosed Alphanumeric Supplement: [U+1F100, U+1F1FF]
// Enclosed Ideographic Supplement: [U+1F200, U+1F2FF]
// Miscellaneous Symbols and Pictographs: [U+1F300, U+1F5FF]
// Supplemental Symbols and Pictographs: [U+1F900, U+1F9FF]
// Emoticons: [U+1F600, U+1F64F]
// Transport and Map Symbols: [U+1F680, U+1F6FF]
if (IS_IN(uc, 0x1D000, 0x1F9FF))
returnValue = YES;
}
}
else if (substring.length > 1 && ls == 0x20E3)
{
// emojis for numbers: number + modifier ls = U+20E3
returnValue = YES;
}
else
{
if ( // Latin-1 Supplement
hs == 0x00A9 || hs == 0x00AE
// General Punctuation
|| hs == 0x203C || hs == 0x2049
// Letterlike Symbols
|| hs == 0x2122 || hs == 0x2139
// Arrows
|| IS_IN(hs, 0x2194, 0x2199) || IS_IN(hs, 0x21A9, 0x21AA)
// Miscellaneous Technical
|| IS_IN(hs, 0x231A, 0x231B) || IS_IN(hs, 0x23E9, 0x23F3) || IS_IN(hs, 0x23F8, 0x23FA) || hs == 0x2328 || hs == 0x23CF
// Geometric Shapes
|| IS_IN(hs, 0x25AA, 0x25AB) || IS_IN(hs, 0x25FB, 0x25FE) || hs == 0x25B6 || hs == 0x25C0
// Miscellaneous Symbols
|| IS_IN(hs, 0x2600, 0x2604) || IS_IN(hs, 0x2614, 0x2615) || IS_IN(hs, 0x2622, 0x2623) || IS_IN(hs, 0x262E, 0x262F)
|| IS_IN(hs, 0x2638, 0x263A) || IS_IN(hs, 0x2648, 0x2653) || IS_IN(hs, 0x2665, 0x2666) || IS_IN(hs, 0x2692, 0x2694)
|| IS_IN(hs, 0x2696, 0x2697) || IS_IN(hs, 0x269B, 0x269C) || IS_IN(hs, 0x26A0, 0x26A1) || IS_IN(hs, 0x26AA, 0x26AB)
|| IS_IN(hs, 0x26B0, 0x26B1) || IS_IN(hs, 0x26BD, 0x26BE) || IS_IN(hs, 0x26C4, 0x26C5) || IS_IN(hs, 0x26CE, 0x26CF)
|| IS_IN(hs, 0x26D3, 0x26D4) || IS_IN(hs, 0x26D3, 0x26D4) || IS_IN(hs, 0x26E9, 0x26EA) || IS_IN(hs, 0x26F0, 0x26F5)
|| IS_IN(hs, 0x26F7, 0x26FA)
|| hs == 0x260E || hs == 0x2611 || hs == 0x2618 || hs == 0x261D || hs == 0x2620 || hs == 0x2626 || hs == 0x262A
|| hs == 0x2660 || hs == 0x2663 || hs == 0x2668 || hs == 0x267B || hs == 0x267F || hs == 0x2699 || hs == 0x26C8
|| hs == 0x26D1 || hs == 0x26FD
// Dingbats
|| IS_IN(hs, 0x2708, 0x270D) || IS_IN(hs, 0x2733, 0x2734) || IS_IN(hs, 0x2753, 0x2755)
|| IS_IN(hs, 0x2763, 0x2764) || IS_IN(hs, 0x2795, 0x2797)
|| hs == 0x2702 || hs == 0x2705 || hs == 0x270F || hs == 0x2712 || hs == 0x2714 || hs == 0x2716 || hs == 0x271D
|| hs == 0x2721 || hs == 0x2728 || hs == 0x2744 || hs == 0x2747 || hs == 0x274C || hs == 0x274E || hs == 0x2757
|| hs == 0x27A1 || hs == 0x27B0 || hs == 0x27BF
// CJK Symbols and Punctuation
|| hs == 0x3030 || hs == 0x303D
// Enclosed CJK Letters and Months
|| hs == 0x3297 || hs == 0x3299
// Supplemental Arrows-B
|| IS_IN(hs, 0x2934, 0x2935)
// Miscellaneous Symbols and Arrows
|| IS_IN(hs, 0x2B05, 0x2B07) || IS_IN(hs, 0x2B1B, 0x2B1C) || hs == 0x2B50 || hs == 0x2B55
)
{
returnValue = YES;
}
}
#undef IS_IN
}];
return returnValue;
}
#endif // FILTER_EMOJIS_IOS_KEYBOARD
@@ -0,0 +1,20 @@
#pragma once
#include <CoreGraphics/CGAffineTransform.h>
#if !PLATFORM_TVOS
ScreenOrientation ConvertToUnityScreenOrientation(UIInterfaceOrientation hwOrient);
UIInterfaceOrientation ConvertToIosScreenOrientation(ScreenOrientation orient);
#endif
#if !PLATFORM_TVOS
UIInterfaceOrientation UIViewControllerInterfaceOrientation(UIViewController* controller);
#endif
ScreenOrientation UIViewControllerOrientation(UIViewController* controller);
CGAffineTransform TransformForOrientation(ScreenOrientation curOrient);
CGAffineTransform TransformBetweenOrientations(ScreenOrientation fromOrient, ScreenOrientation toOrient);
ScreenOrientation OrientationAfterTransform(ScreenOrientation curOrient, CGAffineTransform transform);
void OrientView(UIViewController* host, UIView* view, ScreenOrientation to);
@@ -0,0 +1,159 @@
#include "OrientationSupport.h"
#include <math.h>
CGAffineTransform TransformForOrientation(ScreenOrientation orient)
{
switch (orient)
{
case portrait: return CGAffineTransformIdentity;
case portraitUpsideDown: return CGAffineTransformMakeRotation(M_PI);
case landscapeLeft: return CGAffineTransformMakeRotation(M_PI_2);
case landscapeRight: return CGAffineTransformMakeRotation(-M_PI_2);
default: return CGAffineTransformIdentity;
}
return CGAffineTransformIdentity;
}
CGAffineTransform TransformBetweenOrientations(ScreenOrientation fromOrient, ScreenOrientation toOrient)
{
CGAffineTransform fromTransform = TransformForOrientation(fromOrient);
CGAffineTransform toTransform = TransformForOrientation(toOrient);
return CGAffineTransformConcat(CGAffineTransformInvert(fromTransform), toTransform);
}
#if !PLATFORM_TVOS
UIInterfaceOrientation ConvertToIosScreenOrientation(ScreenOrientation orient)
{
switch (orient)
{
case portrait: return UIInterfaceOrientationPortrait;
case portraitUpsideDown: return UIInterfaceOrientationPortraitUpsideDown;
// landscape left/right have switched values in device/screen orientation
// though unity docs are adjusted with device orientation values, so swap here
case landscapeLeft: return UIInterfaceOrientationLandscapeRight;
case landscapeRight: return UIInterfaceOrientationLandscapeLeft;
case orientationUnknown: return (UIInterfaceOrientation)UIInterfaceOrientationUnknown;
default: return UIInterfaceOrientationPortrait;
}
return UIInterfaceOrientationPortrait;
}
ScreenOrientation ConvertToUnityScreenOrientation(UIInterfaceOrientation orient)
{
switch (orient)
{
case UIInterfaceOrientationPortrait: return portrait;
case UIInterfaceOrientationPortraitUpsideDown: return portraitUpsideDown;
// landscape left/right have switched values in device/screen orientation
// though unity docs are adjusted with device orientation values, so swap here
case UIInterfaceOrientationLandscapeLeft: return landscapeRight;
case UIInterfaceOrientationLandscapeRight: return landscapeLeft;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wswitch"
case UIInterfaceOrientationUnknown: return orientationUnknown;
#pragma clang diagnostic pop
default: return portrait;
}
}
// Replacement for UIViewController.interfaceOrientation which is obsolete since iOS 8.0
UIInterfaceOrientation UIViewControllerInterfaceOrientation(UIViewController* c)
{
CGPoint fixedPoint = [c.view.window.screen.coordinateSpace convertPoint: CGPointMake(0.0, 0.0) toCoordinateSpace: c.view.window.screen.fixedCoordinateSpace];
if (fabs(fixedPoint.x) < FLT_EPSILON)
{
if (fabs(fixedPoint.y) < FLT_EPSILON)
return UIInterfaceOrientationPortrait;
else
return UIInterfaceOrientationLandscapeLeft;
}
else
{
if (fabs(fixedPoint.y) < FLT_EPSILON)
return UIInterfaceOrientationLandscapeRight;
else
return UIInterfaceOrientationPortraitUpsideDown;
}
}
#endif
ScreenOrientation UIViewControllerOrientation(UIViewController* controller)
{
#if PLATFORM_TVOS
return UNITY_TVOS_ORIENTATION;
#else
return ConvertToUnityScreenOrientation(UIViewControllerInterfaceOrientation(controller));
#endif
}
ScreenOrientation OrientationAfterTransform(ScreenOrientation curOrient, CGAffineTransform transform)
{
int rotDeg = (int)::roundf(::atan2f(transform.b, transform.a) * (180 / M_PI));
assert(rotDeg == 0 || rotDeg == 90 || rotDeg == -90 || rotDeg == 180 || rotDeg == -180);
if (rotDeg == 0)
{
return curOrient;
}
else if ((rotDeg == 180) || (rotDeg == -180))
{
if (curOrient == portrait)
return portraitUpsideDown;
else if (curOrient == portraitUpsideDown)
return portrait;
else if (curOrient == landscapeRight)
return landscapeLeft;
else if (curOrient == landscapeLeft)
return landscapeRight;
}
else if (rotDeg == 90)
{
if (curOrient == portrait)
return landscapeLeft;
else if (curOrient == portraitUpsideDown)
return landscapeRight;
else if (curOrient == landscapeRight)
return portrait;
else if (curOrient == landscapeLeft)
return portraitUpsideDown;
}
else if (rotDeg == -90)
{
if (curOrient == portrait)
return landscapeRight;
else if (curOrient == portraitUpsideDown)
return landscapeLeft;
else if (curOrient == landscapeRight)
return portraitUpsideDown;
else if (curOrient == landscapeLeft)
return portrait;
}
::printf("rotation unhandled: %d\n", rotDeg);
return curOrient;
}
void OrientView(UIViewController* host, UIView* view, ScreenOrientation to)
{
ScreenOrientation fromController = UIViewControllerOrientation(host);
CGAffineTransform transform = TransformBetweenOrientations(fromController, to);
// this is for unity-inited orientation. In that case we need to manually adjust bounds if changing portrait/landscape
// the easiest way would be to manually rotate current bounds (to acknowledge the fact that we do NOT rotate controller itself)
// NB: as we use current view bounds we need to use view transform to properly adjust them
CGRect rect = view.bounds;
CGSize ext = CGSizeApplyAffineTransform(rect.size, CGAffineTransformConcat(CGAffineTransformInvert(view.transform), transform));
view.transform = transform;
view.bounds = CGRectMake(0, 0, ::fabs(ext.width), ::fabs(ext.height));
}
@@ -0,0 +1,20 @@
#pragma once
#include "UnityViewControllerBase.h"
@interface SplashScreen : UIImageView
{
}
+ (SplashScreen*)Instance;
@end
@interface SplashScreenController : UnityViewControllerBase
{
}
+ (SplashScreenController*)Instance;
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator;
@end
void ShowSplashScreen(UIWindow* window);
void HideSplashScreen();
@@ -0,0 +1,420 @@
#define UnityGetLaunchScreenXib MyUnityGetLaunchScreenXib
static const char *MyUnityGetLaunchScreenXib() { return NULL; }
#include "SplashScreen.h"
#include "UnityViewControllerBase.h"
#include "OrientationSupport.h"
#include "Unity/ObjCRuntime.h"
#include "UI/UnityView.h"
#include <cstring>
#include "Classes/Unity/UnitySharedDecls.h"
extern "C" const char* UnityGetLaunchScreenXib();
#include <utility>
static SplashScreen* _splash = nil;
static SplashScreenController* _controller = nil;
static bool _isOrientable = false; // true for iPads and iPhone 6+
static bool _usesLaunchscreen = false;
static ScreenOrientation _nonOrientableDefaultOrientation = portrait;
#if !PLATFORM_TVOS
typedef id (*WillRotateToInterfaceOrientationSendFunc)(struct objc_super*, SEL, UIInterfaceOrientation, NSTimeInterval);
typedef id (*DidRotateFromInterfaceOrientationSendFunc)(struct objc_super*, SEL, UIInterfaceOrientation);
#endif
typedef id (*ViewWillTransitionToSizeSendFunc)(struct objc_super*, SEL, CGSize, id<UIViewControllerTransitionCoordinator>);
static const char* GetScaleSuffix(float scale, float maxScale)
{
if (scale > maxScale)
scale = maxScale;
if (scale <= 1.0)
return "";
if (scale <= 2.0)
return "@2x";
return "@3x";
}
static const char* GetOrientationSuffix(const OrientationMask& supportedOrientations, ScreenOrientation orient)
{
bool orientPortrait = (orient == portrait || orient == portraitUpsideDown);
bool orientLandscape = (orient == landscapeLeft || orient == landscapeRight);
bool supportsPortrait = supportedOrientations.portrait || supportedOrientations.portraitUpsideDown;
bool supportsLandscape = supportedOrientations.landscapeLeft || supportedOrientations.landscapeRight;
if (orientPortrait && supportsPortrait)
return "-Portrait";
else if (orientLandscape && supportsLandscape)
return "-Landscape";
else if (supportsPortrait)
return "-Portrait";
else
return "-Landscape";
}
// Returns a launch image name for launch images stored on file system or asset catalog
extern "C" NSArray<NSString*>* GetLaunchImageNames(UIUserInterfaceIdiom idiom, const OrientationMask&supportedOrientations,
const CGSize&screenSize, ScreenOrientation orient, float scale)
{
NSMutableArray<NSString*>* ret = [[NSMutableArray<NSString *> alloc] init];
if (idiom == UIUserInterfaceIdiomPad)
{
// iPads
const char* iOSSuffix = "-700";
const char* orientSuffix = GetOrientationSuffix(supportedOrientations, orient);
const char* scaleSuffix = GetScaleSuffix(scale, 2.0);
[ret addObject: [NSString stringWithFormat: @"LaunchImage%s%s%s~ipad",
iOSSuffix, orientSuffix, scaleSuffix]];
}
else
{
// iPhones
// Note that on pre-iOS 11 using modifiers such as LaunchImage~568h works. Since
// iOS launch image support is quite hard to get right and has _many_ gotchas, we
// just use the old code path on these devices.
if (screenSize.height == 568 || screenSize.width == 568) // iPhone 5
{
[ret addObject: @"LaunchImage-700-568h@2x"];
[ret addObject: @"LaunchImage~568h"];
}
else if (screenSize.height == 667 || screenSize.width == 667) // iPhone 6
{
// note that scale may be 3.0 if display zoom is enabled
if (scale < 2.0) // not expected, but handle just in case. Image name is valid
[ret addObject: @"LaunchImage-800-667h"];
[ret addObject: @"LaunchImage-800-667h@2x"];
[ret addObject: @"LaunchImage~667h"];
}
else if (screenSize.height == 736 || screenSize.width == 736) // iPhone 6+
{
const char* orientSuffix = GetOrientationSuffix(supportedOrientations, orient);
if (scale < 3.0) // not expected, but handle just in case. Image name is valid
[ret addObject: [NSString stringWithFormat: @"LaunchImage-800%s-736h", orientSuffix]];
[ret addObject: [NSString stringWithFormat: @"LaunchImage-800%s-736h@3x", orientSuffix]];
[ret addObject: @"LaunchImage~736h"];
}
else if (screenSize.height == 812 || screenSize.width == 812) // iPhone X
{
const char* orientSuffix = GetOrientationSuffix(supportedOrientations, orient);
if (scale < 3.0) // not expected, but handle just in case. Image name is valid
[ret addObject: [NSString stringWithFormat: @"LaunchImage-1100%s-2436h", orientSuffix]];
[ret addObject: [NSString stringWithFormat: @"LaunchImage-1100%s-2436h@3x", orientSuffix]];
}
if (scale > 1.0)
[ret addObject: @"LaunchImage@2x"];
}
[ret addObject: @"LaunchImage"];
return ret;
}
@implementation SplashScreen
{
UIImageView* m_ImageView;
UIView* m_XibView;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame: frame];
return self;
}
/* The following launch images are produced by Xcode6:
LaunchImage.png
LaunchImage@2x.png
LaunchImage-568h@2x.png
LaunchImage-700@2x.png
LaunchImage-700-568h@2x.png
LaunchImage-700-Landscape@2x~ipad.png
LaunchImage-700-Landscape~ipad.png
LaunchImage-700-Portrait@2x~ipad.png
LaunchImage-700-Portrait~ipad.png
LaunchImage-800-667h@2x.png
LaunchImage-800-Landscape-736h@3x.png
LaunchImage-800-Portrait-736h@3x.png
LaunchImage-1100-Landscape-2436h@3x.png
LaunchImage-1100-Portrait-2436h@3x.png
LaunchImage-Landscape@2x~ipad.png
LaunchImage-Landscape~ipad.png
LaunchImage-Portrait@2x~ipad.png
LaunchImage-Portrait~ipad.png
*/
- (void)updateOrientation:(ScreenOrientation)orient withSupportedOrientations:(const OrientationMask&)supportedOrientations
{
CGFloat scale = UnityScreenScaleFactor([UIScreen mainScreen]);
UnityReportResizeView(self.bounds.size.width * scale, self.bounds.size.height * scale, orient);
// Storyboards should have a view controller to automatically configure orientation
bool hasStoryboard = [[NSBundle mainBundle] pathForResource: @"LaunchScreen" ofType: @"storyboardc"] != nullptr;
if (hasStoryboard)
return;
UIUserInterfaceIdiom idiom = [[UIDevice currentDevice] userInterfaceIdiom];
NSString* xibName = nil;
if (idiom == UIUserInterfaceIdiomPhone)
xibName = @"LaunchScreen-iPhone";
else if (idiom == UIUserInterfaceIdiomPad)
xibName = @"LaunchScreen-iPad";
bool hasLaunchScreen = [[NSBundle mainBundle] pathForResource: xibName ofType: @"nib"] != nullptr;
if (hasLaunchScreen)
{
// Launch screen uses the same aspect-filled image for all iPhone and/or
// all iPads, as configured in Unity. We need a special case if there's
// a launch screen and iOS is configured to use it.
if (self->m_XibView == nil)
{
self->m_XibView = [[[NSBundle mainBundle] loadNibNamed: xibName owner: nil options: nil] objectAtIndex: 0];
[self addSubview: self->m_XibView];
}
return;
}
UIImage* image = nil;
CGSize screenSize = [[UIScreen mainScreen] bounds].size;
CGFloat screenScale = [UIScreen mainScreen].scale;
// For launch images we implement fallback order with multiple images. First we try images via
// [UIImage imageNamed] method and if this fails, we try to load from filesystem directly.
// Note that file system resource names and image names accepted by UIImage are the same.
// Multiple fallbacks are implemented because different iOS versions behave differently and have
// many gotchas that are hard to get right. So we use the images that are present on app bundles
// made with latest version of Xcode as the first priority and then fall back to any image that we
// have used at some time in the past.
NSArray<NSString*>* imageNames = GetLaunchImageNames(idiom, supportedOrientations, screenSize, orient, screenScale);
for (NSString* imageName in imageNames)
{
image = [UIImage imageNamed: imageName];
if (image)
break;
}
if (image == nil)
{
// Old launch image from file
for (NSString* imageName in imageNames)
{
image = [UIImage imageNamed: imageName];
if (image)
break;
NSString* imagePath = [[NSBundle mainBundle] pathForResource: imageName ofType: @"png"];
image = [UIImage imageWithContentsOfFile: imagePath];
if (image)
break;
}
}
// should not ever happen, but just in case
if (image == nil)
return;
if (self->m_ImageView == nil)
{
self->m_ImageView = [[UIImageView alloc] initWithImage: image];
[self addSubview: self->m_ImageView];
}
else
{
self->m_ImageView.image = image;
}
}
- (void)layoutSubviews
{
if (self->m_XibView)
self->m_XibView.frame = self.bounds;
else if (self->m_ImageView)
self->m_ImageView.frame = self.bounds;
}
+ (SplashScreen*)Instance
{
return _splash;
}
- (void)FreeSubviews
{
m_ImageView = nil;
m_XibView = nil;
}
@end
@implementation SplashScreenController
{
OrientationMask _supportedOrientations;
}
- (id)init
{
self = [super init];
if (self)
{
self->_supportedOrientations = { false, false, false, false };
}
return self;
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
ScreenOrientation curOrient = UIViewControllerOrientation(self);
ScreenOrientation newOrient = OrientationAfterTransform(curOrient, [coordinator targetTransform]);
if (_isOrientable)
[_splash updateOrientation: newOrient withSupportedOrientations: self->_supportedOrientations];
[coordinator animateAlongsideTransition: nil completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
if (!_isOrientable)
OrientView(self, _splash, _nonOrientableDefaultOrientation);
}];
[super viewWillTransitionToSize: size withTransitionCoordinator: coordinator];
}
- (void)create:(UIWindow*)window
{
NSArray* supportedOrientation = [[[NSBundle mainBundle] infoDictionary] objectForKey: @"UISupportedInterfaceOrientations"];
bool isIphone = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone;
bool isIpad = !isIphone;
// splash will be shown way before unity is inited so we need to override autorotation handling with values read from info.plist
self->_supportedOrientations.portrait = [supportedOrientation containsObject: @"UIInterfaceOrientationPortrait"];
self->_supportedOrientations.portraitUpsideDown = [supportedOrientation containsObject: @"UIInterfaceOrientationPortraitUpsideDown"];
self->_supportedOrientations.landscapeLeft = [supportedOrientation containsObject: @"UIInterfaceOrientationLandscapeRight"];
self->_supportedOrientations.landscapeRight = [supportedOrientation containsObject: @"UIInterfaceOrientationLandscapeLeft"];
CGSize size = [[UIScreen mainScreen] bounds].size;
// iPads and iPhone Plus models and iOS11 have orientable splash screen
_isOrientable = isIpad || (size.height == 736 || size.width == 736) || UnityiOS110orNewer();
// Launch screens are used only on iOS8+ iPhones
const char* xib = UnityGetLaunchScreenXib();
#if !PLATFORM_TVOS
_usesLaunchscreen = false;
if (xib != NULL)
{
const char* expectedName = isIphone ? "LaunchScreen-iPhone" : "LaunchScreen-iPad";
if (std::strcmp(xib, expectedName) == 0)
_usesLaunchscreen = true;
}
#else
_usesLaunchscreen = false;
#endif
if (!(self->_supportedOrientations.portrait || self->_supportedOrientations.portraitUpsideDown))
_nonOrientableDefaultOrientation = landscapeLeft;
else
_nonOrientableDefaultOrientation = portrait;
_splash = [[SplashScreen alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
_splash.contentScaleFactor = [UIScreen mainScreen].scale;
if (_isOrientable)
{
_splash.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_splash.autoresizesSubviews = YES;
}
else if (self->_supportedOrientations.portrait || self->_supportedOrientations.portraitUpsideDown)
{
self->_supportedOrientations.landscapeLeft = false;
self->_supportedOrientations.landscapeRight = false;
}
// On non-orientable devices with launch screens, landscapeLeft is always used if both
// landscapeRight and landscapeLeft are enabled
if (!_isOrientable && _supportedOrientations.landscapeRight)
{
if (self->_supportedOrientations.landscapeLeft)
self->_supportedOrientations.landscapeRight = false;
else
_nonOrientableDefaultOrientation = landscapeRight;
}
window.rootViewController = self;
self.view = _splash;
[window addSubview: _splash];
[window bringSubviewToFront: _splash];
ScreenOrientation orient = UIViewControllerOrientation(self);
[_splash updateOrientation: orient withSupportedOrientations: self->_supportedOrientations];
if (!_isOrientable)
orient = _nonOrientableDefaultOrientation;
// fix iPhone 5,6 launch images (only in portrait) from being stretched
if (isIphone && _isOrientable && !((size.height == 568 || size.width == 568) || (size.height == 667 || size.width == 667)))
orient = portrait;
OrientView([SplashScreenController Instance], _splash, orient);
}
- (BOOL)shouldAutorotate
{
return YES;
}
- (NSUInteger)supportedInterfaceOrientations
{
NSUInteger ret = 0;
if (self->_supportedOrientations.portrait)
ret |= (1 << UIInterfaceOrientationPortrait);
if (self->_supportedOrientations.portraitUpsideDown)
ret |= (1 << UIInterfaceOrientationPortraitUpsideDown);
if (self->_supportedOrientations.landscapeLeft)
ret |= (1 << UIInterfaceOrientationLandscapeRight);
if (self->_supportedOrientations.landscapeRight)
ret |= (1 << UIInterfaceOrientationLandscapeLeft);
return ret;
}
+ (SplashScreenController*)Instance
{
return _controller;
}
@end
void ShowSplashScreen(UIWindow* window)
{
bool hasStoryboard = [[NSBundle mainBundle] pathForResource: @"LaunchScreen" ofType: @"storyboardc"] != nullptr;
if (hasStoryboard)
{
UIStoryboard *storyboard = [UIStoryboard storyboardWithName: @"LaunchScreen" bundle: [NSBundle mainBundle]];
_controller = [storyboard instantiateInitialViewController];
window.rootViewController = _controller;
}
else
{
_controller = [[SplashScreenController alloc] init];
[_controller create: window];
}
[window makeKeyAndVisible];
}
void HideSplashScreen()
{
if (_splash)
{
[_splash removeFromSuperview];
[_splash FreeSubviews];
}
_splash = nil;
_controller = nil;
}
@@ -0,0 +1,18 @@
#if PLATFORM_IOS
// This definition is here only for compiler to know about selector requestReview
@interface UnityStoreReviewController
+ requestReview;
@end
bool UnityRequestStoreReview()
{
Class classSKStoreReviewController = NSClassFromString(@"SKStoreReviewController");
if (!classSKStoreReviewController || ![classSKStoreReviewController respondsToSelector: @selector(requestReview)])
return false;
[classSKStoreReviewController performSelector: @selector(requestReview)];
return true;
}
#endif
@@ -0,0 +1,105 @@
#pragma once
#include "UnityAppController.h"
#include <AvailabilityMacros.h>
@interface UnityAppController (ViewHandling)
// tweaking view hierarchy and handling of orientation
// there are 3 main uses cases regarding UI handling:
//
// 1. normal game case: you shouldnt care about all this at all
//
// 2. you need some not-so-trivial overlayed views and/or minor UI tweaking
// most likely all you need is to subscribe to "orientation changed" notification
// or in case you have per-orientation UI logic override willTransitionToViewController
//
// 3. you create UI-rich app where unity view is just one of many
// in that case you might want to create your own controllers and implement transitions on top
// also instead of orientUnity: (and Screen.orientation in script) you should use orientInterface
// override this if you need customized unityview (subclassing)
// if you simply want different root view, tweak view hierarchy in createAutorotatingUnityViewController
- (UnityView*)createUnityView;
// for view controllers we discern between platforms that do support orientation (e.g. iOS) and the ones that dont (e.g. tvOS)
// both have concept of "default" view controller: for iOS it will be auto-rotating one (with possible constraints) and "simple" controller otherwise
// in case of supporting orientation we will discern case of fixed-orientation view controller (that seems to be the only way to handle it robustly)
// _unityView will be inited at the point of calling any of "create view controller" methods
// please note that these are actual "create" methods: there is no need to tweak hierarchy right away
- (UIViewController*)createUnityViewControllerDefault;
#if UNITY_SUPPORT_ROTATION
- (UIViewController*)createUnityViewControllerForOrientation:(UIInterfaceOrientation)orient;
#endif
#if UNITY_SUPPORT_ROTATION
// if you override these you need to call super
// if your root controller is not subclassed from UnityViewControllerBase, call these when rotation is happening
- (void)interfaceWillChangeOrientationTo:(UIInterfaceOrientation)toInterfaceOrientation;
- (void)interfaceDidChangeOrientationFrom:(UIInterfaceOrientation)fromInterfaceOrientation;
#endif
// handling of changing ViewControllers:
// willStartWithViewController: will be called on startup, when creating view hierarchy
// willTransitionToViewController:fromViewController: didTransitionToViewController:fromViewController:
// are called before/after we are doing some magic to switch to new root controller due to forced orientation change
// by default:
// willStartWithViewController: will make _unityView as root view
// willTransitionToViewController:fromViewController: will do nothing
// didTransitionToViewController:fromViewController: will send orientation events to unity view
// you can use them to tweak view hierarchy if needed
- (void)willStartWithViewController:(UIViewController*)controller;
- (void)willTransitionToViewController:(UIViewController*)toController fromViewController:(UIViewController*)fromController;
- (void)didTransitionToViewController:(UIViewController*)toController fromViewController:(UIViewController*)fromController;
// override this if you want to have custom snapshot view.
// by default it will capture the frame drawn inside applicationWillResignActive specifically to let app respond to OnApplicationPause
// will be called on every applicationWillResignActive; returned view will be released in applicationDidBecomeActive
// NB: case of returning nil will be handled gracefully
- (UIView*)createSnapshotView;
// you should not override these methods
// creates initial UI hierarchy (e.g. splash screen) and calls willStartWithViewController
- (void)createUI;
// shows game itself (hides splash, and bring _rootView to front)
- (void)showGameUI;
// returns the topmost presentedViewController if there is one, or just rootViewController
- (UIViewController*)topMostController;
// will create or return from cache correct view controller for requested orientation
- (UIViewController*)createRootViewController;
// old deprecated methods: no longer used
// the caveat is: there are some issues in clang related to method deprecation
// which results in warnings not being generated for overriding deprecated methods (in some circumstances).
// so instead of deprecating these methods we just remove them and will check at runtime if user have them and whine about it
//- (UnityView*)createUnityViewImpl DEPRECATED_MSG_ATTRIBUTE("Will not be called. Override createUnityView");
//- (void)createViewHierarchyImpl DEPRECATED_MSG_ATTRIBUTE("Will not be called. Override willStartWithViewController");
//- (void)createViewHierarchy DEPRECATED_MSG_ATTRIBUTE("Is not implemented. Use createUI");
@end
#if UNITY_SUPPORT_ROTATION
@interface UnityAppController (OrientationSupport)
// will create or return from cache correct view controller for given orientation
- (UIViewController*)createRootViewControllerForOrientation:(UIInterfaceOrientation)orientation;
// forcibly orient interface
- (void)orientInterface:(UIInterfaceOrientation)orient;
// check unity requested orientation and applies it
- (void)checkOrientationRequest;
- (void)orientUnity:(UIInterfaceOrientation)orient __deprecated_msg("use orientInterface instead.");
@end
#endif
@@ -0,0 +1,418 @@
#include "UnityAppController+ViewHandling.h"
#include "UnityAppController+Rendering.h"
#include "UI/OrientationSupport.h"
#include "UI/UnityView.h"
#include "UI/UnityViewControllerBase.h"
#include "Unity/DisplayManager.h"
// TEMP: ?
#include "UI/ActivityIndicator.h"
#include "UI/SplashScreen.h"
#include "UI/Keyboard.h"
#include <utility>
extern bool _skipPresent;
extern bool _unityAppReady;
@implementation UnityAppController (ViewHandling)
#if UNITY_SUPPORT_ROTATION
// special case for when we DO know the app orientation, but dont get it through normal mechanism (UIViewController orientation handling)
// how can this happen:
// 1. On startup: ios is not sending "change orientation" notifications on startup (but rather we "start" in correct one already)
// 2. When using presentation controller it can override orientation constraints, so on dismissing we need to tweak app orientation;
// pretty much like startup situation UIViewController would have correct orientation, and app will be out-of-sync
- (void)updateAppOrientation:(UIInterfaceOrientation)orientation
{
_curOrientation = orientation;
[_unityView willRotateToOrientation: orientation fromOrientation: (UIInterfaceOrientation)UIInterfaceOrientationUnknown];
[_unityView didRotate];
}
#endif
- (UnityView*)createUnityView
{
return [[UnityView alloc] initFromMainScreen];
}
- (UIViewController*)createUnityViewControllerDefault
{
UnityDefaultViewController* ret = [[UnityDefaultViewController alloc] init];
#if PLATFORM_TVOS
// This enables game controller use in on-screen keyboard
ret.controllerUserInteractionEnabled = YES;
#endif
return ret;
}
#if UNITY_SUPPORT_ROTATION
- (UIViewController*)createUnityViewControllerForOrientation:(UIInterfaceOrientation)orient
{
switch (orient)
{
case UIInterfaceOrientationPortrait: return [[UnityPortraitOnlyViewController alloc] init];
case UIInterfaceOrientationPortraitUpsideDown: return [[UnityPortraitUpsideDownOnlyViewController alloc] init];
case UIInterfaceOrientationLandscapeLeft: return [[UnityLandscapeLeftOnlyViewController alloc] init];
case UIInterfaceOrientationLandscapeRight: return [[UnityLandscapeRightOnlyViewController alloc] init];
default: NSAssert(false, @"bad UIInterfaceOrientation provided");
}
return nil;
}
#endif
- (UIViewController*)createRootViewController
{
UIViewController* ret = nil;
if (!UNITY_SUPPORT_ROTATION || UnityShouldAutorotate())
{
if (_viewControllerForOrientation[0] == nil)
_viewControllerForOrientation[0] = [self createUnityViewControllerDefault];
ret = _viewControllerForOrientation[0];
}
#if UNITY_SUPPORT_ROTATION
if (ret == nil)
{
UIInterfaceOrientation orientation = ConvertToIosScreenOrientation((ScreenOrientation)UnityRequestedScreenOrientation());
ret = [self createRootViewControllerForOrientation: orientation];
}
#endif
return ret;
}
- (UIViewController*)topMostController
{
UIViewController *topController = self.window.rootViewController;
while (topController.presentedViewController)
topController = topController.presentedViewController;
return topController;
}
- (void)willStartWithViewController:(UIViewController*)controller
{
_unityView.contentScaleFactor = UnityScreenScaleFactor([UIScreen mainScreen]);
_unityView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_rootController.view = _rootView = _unityView;
}
- (void)willTransitionToViewController:(UIViewController*)toController fromViewController:(UIViewController*)fromController
{
}
- (void)didTransitionToViewController:(UIViewController*)toController fromViewController:(UIViewController*)fromController
{
#if UNITY_SUPPORT_ROTATION
// when transitioning between view controllers ios will not send reorient events (because they are bound to controllers, not view)
// so we imitate them here so unity view can update its size/orientation
[_unityView willRotateToOrientation: UIViewControllerInterfaceOrientation(toController) fromOrientation: ConvertToIosScreenOrientation(_unityView.contentOrientation)];
[_unityView didRotate];
// NB: this is both important and insane at the same time (that we have several places to keep current orentation and we need to sync them)
_curOrientation = UIViewControllerInterfaceOrientation(toController);
#endif
}
- (UIView*)createSnapshotView
{
// Snapshot API appeared on iOS 7, however before iOS 8 tweaking hierarchy like that on going to
// background results in all kind of weird things when going back to foreground so we do snapshotting
// only on iOS 8 and newer.
// Note that on iPads with iOS 9 or later (up to iOS 10.2 at least) there's a bug in the iOS
// compositor: any use of -[UIView snapshotViewAfterScreenUpdates] causes black screen being shown
// temporarily when 4 finger gesture to swipe to another app in the task switcher is being performed slowly
#if UNITY_SNAPSHOT_VIEW_ON_APPLICATION_PAUSE
return [_rootView snapshotViewAfterScreenUpdates: YES];
#else
return nil;
#endif
}
- (void)createUI
{
NSAssert(_unityView != nil, @"_unityView should be inited at this point");
NSAssert(_window != nil, @"_window should be inited at this point");
_rootController = [self createRootViewController];
[self willStartWithViewController: _rootController];
NSAssert(_rootView != nil, @"_rootView should be inited at this point");
NSAssert(_rootController != nil, @"_rootController should be inited at this point");
[_window makeKeyAndVisible];
[UIView setAnimationsEnabled: NO];
// TODO: extract it?
ShowSplashScreen(_window);
#if UNITY_SUPPORT_ROTATION
// to be able to query orientation from view controller we should actually show it.
// at this point we can only show splash screen, so update app orientation after we started showing it
// NB: _window.rootViewController = splash view controller (not _rootController)
[self updateAppOrientation: ConvertToIosScreenOrientation(UIViewControllerOrientation(_window.rootViewController))];
#endif
NSNumber* style = [[[NSBundle mainBundle] infoDictionary] objectForKey: @"Unity_LoadingActivityIndicatorStyle"];
ShowActivityIndicator([SplashScreen Instance], style ? [style intValue] : -1);
NSNumber* vcControlled = [[[NSBundle mainBundle] infoDictionary] objectForKey: @"UIViewControllerBasedStatusBarAppearance"];
if (vcControlled && ![vcControlled boolValue])
printf_console("\nSetting UIViewControllerBasedStatusBarAppearance to NO is no longer supported.\n"
"Apple actively discourages that, and all application-wide methods of changing status bar appearance are deprecated\n\n"
);
}
- (void)showGameUI
{
HideActivityIndicator();
HideSplashScreen();
// make sure that we start up with correctly created/inited rendering surface
// NB: recreateRenderingSurface won't go into rendering because _unityAppReady is false
#if UNITY_SUPPORT_ROTATION
[self checkOrientationRequest];
#endif
[_unityView recreateRenderingSurface];
// UI hierarchy
[_window addSubview: _rootView];
_window.rootViewController = _rootController;
[_window bringSubviewToFront: _rootView];
#if UNITY_SUPPORT_ROTATION
// to be able to query orientation from view controller we should actually show it.
// at this point we finally started to show game view controller. Just in case update orientation again
[self updateAppOrientation: ConvertToIosScreenOrientation(UIViewControllerOrientation(_rootController))];
#endif
// why we set level ready only now:
// surface recreate will try to repaint if this var is set (poking unity to do it)
// but this frame now is actually the first one we want to process/draw
// so all the recreateSurface before now (triggered by reorientation) should simply change extents
_unityAppReady = true;
// why we skip present:
// this will be the first frame to draw, so Start methods will be called
// and we want to properly handle resolution request in Start (which might trigger surface recreate)
// NB: we want to draw right after showing window, to avoid black frame creeping in
_skipPresent = true;
if (!UnityIsPaused())
UnityRepaint();
_skipPresent = false;
[self repaint];
[UIView setAnimationsEnabled: YES];
}
- (void)transitionToViewController:(UIViewController*)vc
{
[self willTransitionToViewController: vc fromViewController: _rootController];
// first: remove from view hierarchy.
// if we simply hide the window before assigning the new view controller, it will cause black frame flickering
// on the other hand, hiding the window is important by itself to better signal the intent to iOS
// e.g. unless we hide the window view, safeArea might stop working (due to bug in iOS if we're to speculate)
// due to that we do this hide/unhide sequence: we want to to make it hidden, but still unhide it before changing window view controller.
_window.hidden = YES;
_window.hidden = NO;
_rootController.view = nil;
_window.rootViewController = nil;
// second: assign new root controller (and view hierarchy with that), restore bounds
_rootController = _window.rootViewController = vc;
_rootController.view = _rootView;
_window.bounds = [UIScreen mainScreen].bounds;
// required for iOS 8, otherwise view bounds will be incorrect
_rootView.bounds = _window.bounds;
_rootView.center = _window.center;
// third: restore window as key and layout subviews to finalize size changes
[_window makeKeyAndVisible];
[_window layoutSubviews];
[self didTransitionToViewController: vc fromViewController: _rootController];
}
#if UNITY_SUPPORT_ROTATION
- (void)interfaceWillChangeOrientationTo:(UIInterfaceOrientation)toInterfaceOrientation
{
UIInterfaceOrientation fromInterfaceOrientation = _curOrientation;
_curOrientation = toInterfaceOrientation;
[_unityView willRotateToOrientation: toInterfaceOrientation fromOrientation: fromInterfaceOrientation];
}
- (void)interfaceDidChangeOrientationFrom:(UIInterfaceOrientation)fromInterfaceOrientation
{
[_unityView didRotate];
}
#endif
#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
- (void)executeForEveryViewController:(void(^)(UIViewController*))callback
{
for (unsigned i = 0; i < ARRAY_SIZE(_viewControllerForOrientation); ++i)
{
UIViewController* vc = _viewControllerForOrientation[i];
if (vc)
callback(vc);
}
}
- (void)notifyHideHomeButtonChange
{
// Note that we need to update all view controllers because UIKit won't necessarily
// update the properties of view controllers when orientation is changed.
#if PLATFORM_IOS && UNITY_HAS_IOSSDK_11_0
if (@available(iOS 11.0, *))
{
[self executeForEveryViewController: ^(UIViewController* vc)
{
// setNeedsUpdateOfHomeIndicatorAutoHidden is not implemented on iOS 11.0.
// The bug has been fixed in iOS 11.0.1. See http://www.openradar.me/35127134
if ([vc respondsToSelector: @selector(setNeedsUpdateOfHomeIndicatorAutoHidden)])
[vc setNeedsUpdateOfHomeIndicatorAutoHidden];
}];
}
#endif
}
- (void)notifyDeferSystemGesturesChange
{
#if PLATFORM_IOS && UNITY_HAS_IOSSDK_11_0
if (@available(iOS 11.0, *))
{
[self executeForEveryViewController: ^(UIViewController* vc)
{
[vc setNeedsUpdateOfScreenEdgesDeferringSystemGestures];
}];
}
#endif
}
@end
#if UNITY_SUPPORT_ROTATION
@implementation UnityAppController (OrientationSupport)
- (UIViewController*)createRootViewControllerForOrientation:(UIInterfaceOrientation)orientation
{
NSAssert(orientation != 0, @"Bad UIInterfaceOrientation provided");
if (_viewControllerForOrientation[orientation] == nil)
_viewControllerForOrientation[orientation] = [self createUnityViewControllerForOrientation: orientation];
return _viewControllerForOrientation[orientation];
}
- (void)checkOrientationRequest
{
if (!UnityHasOrientationRequest() && !UnityShouldChangeAllowedOrientations())
return;
// normally we want to call attemptRotationToDeviceOrientation to tell iOS that we changed orientation constraints
// but if the current orientation is disabled we need special processing, as iOS will simply ignore us
// the only good/robust way is to simply recreate "autorotating" view controller and transition to it if needed
// please note that we want to trigger "orientation request" code path if we recreate autorotating view controller
bool changeOrient = UnityHasOrientationRequest();
if (UnityShouldChangeAllowedOrientations())
{
// so we can say that the only corner case is when we are autorotating and will autorotate
// AND changed allowed orientations while keeping current allowed
// in that case we simply trigger attemptRotationToDeviceOrientation and we are done
// please note that it this can happen when current *device* orientation is disabled (and we want to enable it)
NSUInteger rootOrient = 1 << UIViewControllerInterfaceOrientation(self.rootViewController);
if (UnityShouldAutorotate() && _rootController == _viewControllerForOrientation[0] && (rootOrient & EnabledAutorotationInterfaceOrientations()))
{
[UIViewController attemptRotationToDeviceOrientation];
return;
}
// otherwise we recreate default autorotating view controller
// please note that below we will check if root controller still equals _viewControllerForOrientation[0]
// in that case (we update _viewControllerForOrientation[0]) the check will fail and trigger transition (as expected)
// you may look at this check as "are we autorotating with same constraints"
_viewControllerForOrientation[0] = [self createUnityViewControllerDefault];
changeOrient = true;
}
if (changeOrient)
{
if (UnityShouldAutorotate())
{
if (_viewControllerForOrientation[0] == nil)
_viewControllerForOrientation[0] = [self createUnityViewControllerDefault];
if (_rootController != _viewControllerForOrientation[0])
[self transitionToViewController: _viewControllerForOrientation[0]];
[UIViewController attemptRotationToDeviceOrientation];
}
else
{
ScreenOrientation requestedOrient = (ScreenOrientation)UnityRequestedScreenOrientation();
[self orientInterface: ConvertToIosScreenOrientation(requestedOrient)];
}
}
UnityOrientationRequestWasCommitted();
}
- (void)orientInterface:(UIInterfaceOrientation)orient
{
if (_unityAppReady)
UnityFinishRendering();
[KeyboardDelegate StartReorientation];
[CATransaction begin];
{
UIInterfaceOrientation oldOrient = _curOrientation;
UIInterfaceOrientation newOrient = orient;
[self interfaceWillChangeOrientationTo: newOrient];
[self transitionToViewController: [self createRootViewControllerForOrientation: newOrient]];
[self interfaceDidChangeOrientationFrom: oldOrient];
[UIApplication sharedApplication].statusBarOrientation = orient;
}
[CATransaction commit];
[KeyboardDelegate FinishReorientation];
}
- (void)orientUnity:(UIInterfaceOrientation)orient
{
[self orientInterface: orient];
}
@end
#endif
extern "C" void UnityNotifyHideHomeButtonChange()
{
[GetAppController() notifyHideHomeButtonChange];
}
extern "C" void UnityNotifyDeferSystemGesturesChange()
{
[GetAppController() notifyDeferSystemGesturesChange];
}
@@ -0,0 +1,222 @@
#import "UnityView.h"
#include "UI/Keyboard.h"
static NSArray* keyboardCommands = nil;
static int pressedButton = 0;
@interface UnityView (Keyboard)
@end
@implementation UnityView (Keyboard)
- (void)createKeyboard
{
// only English keyboard layout is supported
NSString* baseLayout = @"1234567890-=qwertyuiop[]asdfghjkl;'\\`zxcvbnm,./!@#$%^&*()_+{}:\"|<>?~ \t\r\b\\";
NSString* numpadLayout = @"1234567890-=*+/.\r";
size_t sizeOfKeyboardCommands = baseLayout.length + numpadLayout.length + 11;
NSMutableArray* commands = [NSMutableArray arrayWithCapacity: sizeOfKeyboardCommands];
for (NSInteger i = 0; i < baseLayout.length; ++i)
{
NSString* input = [baseLayout substringWithRange: NSMakeRange(i, 1)];
[commands addObject: [UIKeyCommand keyCommandWithInput: input modifierFlags: kNilOptions action: @selector(handleCommand:)]];
}
for (NSInteger i = 0; i < numpadLayout.length; ++i)
{
NSString* input = [numpadLayout substringWithRange: NSMakeRange(i, 1)];
[commands addObject: [UIKeyCommand keyCommandWithInput: input modifierFlags: UIKeyModifierNumericPad action: @selector(handleCommand:)]];
}
// up, down, left, right, esc
[commands addObject: [UIKeyCommand keyCommandWithInput: UIKeyInputUpArrow modifierFlags: kNilOptions action: @selector(handleCommand:)]];
[commands addObject: [UIKeyCommand keyCommandWithInput: UIKeyInputDownArrow modifierFlags: kNilOptions action: @selector(handleCommand:)]];
[commands addObject: [UIKeyCommand keyCommandWithInput: UIKeyInputLeftArrow modifierFlags: kNilOptions action: @selector(handleCommand:)]];
[commands addObject: [UIKeyCommand keyCommandWithInput: UIKeyInputRightArrow modifierFlags: kNilOptions action: @selector(handleCommand:)]];
[commands addObject: [UIKeyCommand keyCommandWithInput: UIKeyInputEscape modifierFlags: kNilOptions action: @selector(handleCommand:)]];
// caps Lock, shift, control, option, command
[commands addObject: [UIKeyCommand keyCommandWithInput: @"" modifierFlags: UIKeyModifierAlphaShift action: @selector(handleCommand:)]];
[commands addObject: [UIKeyCommand keyCommandWithInput: @"" modifierFlags: UIKeyModifierShift action: @selector(handleCommand:)]];
[commands addObject: [UIKeyCommand keyCommandWithInput: @"" modifierFlags: UIKeyModifierControl action: @selector(handleCommand:)]];
[commands addObject: [UIKeyCommand keyCommandWithInput: @"" modifierFlags: UIKeyModifierAlternate action: @selector(handleCommand:)]];
[commands addObject: [UIKeyCommand keyCommandWithInput: @"" modifierFlags: UIKeyModifierCommand action: @selector(handleCommand:)]];
keyboardCommands = commands.copy;
}
// UIKeyCommand can't handle key up events,
// So we're simulating key up event in case if any other button is pressed or UIView calls keyCommands method.
// Because of this there is no way to handle simultanious key presses.
- (void)releaseButton
{
if (pressedButton != 0)
{
UnitySetKeyState(pressedButton, false);
pressedButton = 0;
}
}
- (NSArray*)keyCommands
{
[self releaseButton];
//keyCommands take controll of buttons over UITextView, that's why need to return nil if text input field is active
if ([[KeyboardDelegate Instance] status] == Visible)
{
return nil;
}
if (keyboardCommands == nil)
{
[self createKeyboard];
}
return keyboardCommands;
}
- (bool)isValidCodeForButton:(int)code
{
return (code > 0 && code < 128);
}
- (void)handleCommand:(UIKeyCommand *)command
{
[self releaseButton];
NSString* input = command.input;
UIKeyModifierFlags modifierFlags = command.modifierFlags;
char inputChar = ([input length] > 0) ? [input characterAtIndex: 0] : 0;
int code = (int)inputChar; // ASCII code
if (![self isValidCodeForButton: code])
{
code = 0;
}
if ((modifierFlags & UIKeyModifierAlphaShift) != 0)
code = UnityStringToKey("caps lock");
if ((modifierFlags & UIKeyModifierShift) != 0)
code = UnityStringToKey("left shift");
if ((modifierFlags & UIKeyModifierControl) != 0)
code = UnityStringToKey("left ctrl");
if ((modifierFlags & UIKeyModifierAlternate) != 0)
code = UnityStringToKey("left alt");
if ((modifierFlags & UIKeyModifierCommand) != 0)
code = UnityStringToKey("left cmd");
if ((modifierFlags & UIKeyModifierNumericPad) != 0)
{
switch (inputChar)
{
case '0':
code = UnityStringToKey("[0]");
break;
case '1':
code = UnityStringToKey("[1]");
break;
case '2':
code = UnityStringToKey("[2]");
break;
case '3':
code = UnityStringToKey("[3]");
break;
case '4':
code = UnityStringToKey("[4]");
break;
case '5':
code = UnityStringToKey("[5]");
break;
case '6':
code = UnityStringToKey("[6]");
break;
case '7':
code = UnityStringToKey("[7]");
break;
case '8':
code = UnityStringToKey("[8]");
break;
case '9':
code = UnityStringToKey("[9]");
break;
case '-':
code = UnityStringToKey("[-]");
break;
case '=':
code = UnityStringToKey("equals");
break;
case '*':
code = UnityStringToKey("[*]");
break;
case '+':
code = UnityStringToKey("[+]");
break;
case '/':
code = UnityStringToKey("[/]");
break;
case '.':
code = UnityStringToKey("[.]");
break;
case '\r':
code = UnityStringToKey("enter");
break;
default:
break;
}
}
if (input == UIKeyInputUpArrow)
code = UnityStringToKey("up");
else if (input == UIKeyInputDownArrow)
code = UnityStringToKey("down");
else if (input == UIKeyInputRightArrow)
code = UnityStringToKey("right");
else if (input == UIKeyInputLeftArrow)
code = UnityStringToKey("left");
else if (input == UIKeyInputEscape)
code = UnityStringToKey("escape");
UnitySetKeyState(code, true);
pressedButton = code;
}
#if PLATFORM_TVOS
- (int)pressTypeToCode:(UIPress *)press
{
if ([press type] == UIPressTypeUpArrow)
return UnityStringToKey("up");
else if ([press type] == UIPressTypeDownArrow)
return UnityStringToKey("down");
else if ([press type] == UIPressTypeRightArrow)
return UnityStringToKey("right");
else if ([press type] == UIPressTypeLeftArrow)
return UnityStringToKey("left");
else if ([press type] == UIPressTypeSelect)
return UnityStringToKey("joystick button 14");
else if ([press type] == UIPressTypePlayPause)
return UnityStringToKey("joystick button 15");
else if ([press type] == UIPressTypeMenu)
return UnityStringToKey("joystick button 0");
return 0;
}
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event
{
for (UIPress *press in presses)
{
UnitySetKeyState([self pressTypeToCode: press], true);
}
}
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event
{
for (UIPress *press in presses)
{
UnitySetKeyState([self pressTypeToCode: press], false);
}
}
#endif
@end
@@ -0,0 +1,15 @@
#pragma once
@interface UnityView (iOS)
// will simply update content orientation (it might be tweaked in layoutSubviews, due to disagreement between unity and view controller)
- (void)willRotateToOrientation:(UIInterfaceOrientation)toOrientation fromOrientation:(UIInterfaceOrientation)fromOrientation;
// will recreate gles backing if needed and repaint once to make sure we dont have black frame creeping in
- (void)didRotate;
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event;
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event;
- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event;
- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event;
@end
@@ -0,0 +1,49 @@
#if PLATFORM_IOS
#import "UnityView.h"
#import "UnityAppController+Rendering.h"
#include "OrientationSupport.h"
extern bool _unityAppReady;
@interface UnityView ()
@property (nonatomic, readwrite) ScreenOrientation contentOrientation;
@end
@implementation UnityView (iOS)
- (void)willRotateToOrientation:(UIInterfaceOrientation)toOrientation fromOrientation:(UIInterfaceOrientation)fromOrientation;
{
// to support the case of interface and unity content orientation being different
// we will cheat a bit:
// we will calculate transform between interface orientations and apply it to unity view orientation
// you can still tweak unity view as you see fit in AppController, but this is what you want in 99% of cases
ScreenOrientation to = ConvertToUnityScreenOrientation(toOrientation);
ScreenOrientation from = ConvertToUnityScreenOrientation(fromOrientation);
if (fromOrientation == UIInterfaceOrientationUnknown)
_curOrientation = to;
else
_curOrientation = OrientationAfterTransform(_curOrientation, TransformBetweenOrientations(from, to));
_viewIsRotating = YES;
}
- (void)didRotate
{
if (_shouldRecreateView)
{
[self recreateRenderingSurface];
}
_viewIsRotating = NO;
}
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { UnitySendTouchesBegin(touches, event); }
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { UnitySendTouchesEnded(touches, event); }
- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { UnitySendTouchesCancelled(touches, event); }
- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event { UnitySendTouchesMoved(touches, event); }
@end
#endif // PLATFORM_IOS
@@ -0,0 +1,15 @@
#pragma once
@interface UnityView (tvOS)
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event;
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event;
- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event;
- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event;
#if UNITY_TVOS_SIMULATOR_FAKE_REMOTE
- (void)pressesBegan:(NSSet<UIPress*>*)presses withEvent:(UIEvent*)event;
- (void)pressesEnded:(NSSet<UIPress*>*)presses withEvent:(UIEvent*)event;
#endif
@end
@@ -0,0 +1,66 @@
#if PLATFORM_TVOS
#import "UnityView.h"
#include "iphone_Sensors.h"
@implementation UnityView (tvOS)
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
{
#if UNITY_TVOS_SIMULATOR_FAKE_REMOTE
ReportSimulatedRemoteTouchesBegan(self, touches);
#endif
if (UnityGetAppleTVRemoteTouchesEnabled())
UnitySendTouchesBegin(touches, event);
}
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event
{
#if UNITY_TVOS_SIMULATOR_FAKE_REMOTE
ReportSimulatedRemoteTouchesEnded(self, touches);
#endif
if (UnityGetAppleTVRemoteTouchesEnabled())
UnitySendTouchesEnded(touches, event);
}
- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event
{
#if UNITY_TVOS_SIMULATOR_FAKE_REMOTE
ReportSimulatedRemoteTouchesEnded(self, touches);
#endif
if (UnityGetAppleTVRemoteTouchesEnabled())
UnitySendTouchesCancelled(touches, event);
}
- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
#if UNITY_TVOS_SIMULATOR_FAKE_REMOTE
ReportSimulatedRemoteTouchesMoved(self, touches);
#endif
if (UnityGetAppleTVRemoteTouchesEnabled())
UnitySendTouchesMoved(touches, event);
}
#if UNITY_TVOS_SIMULATOR_FAKE_REMOTE
- (void)pressesBegan:(NSSet<UIPress*>*)presses withEvent:(UIEvent*)event
{
for (UIPress *press in presses)
ReportSimulatedRemoteButtonPress(press.type);
}
- (void)pressesEnded:(NSSet<UIPress*>*)presses withEvent:(UIEvent*)event
{
for (UIPress *press in presses)
ReportSimulatedRemoteButtonRelease(press.type);
}
#endif
@end
#endif // PLATFORM_TVOS
@@ -0,0 +1,48 @@
#pragma once
@interface UnityRenderingView : UIView
{
}
+ (void)InitializeForAPI:(UnityRenderingAPI)api;
@end
@interface UnityView : UnityRenderingView
{
@private ScreenOrientation _curOrientation;
@private BOOL _shouldRecreateView;
@private BOOL _viewIsRotating;
}
// we take scale factor into account because gl backbuffer size depends on it
- (id)initWithFrame:(CGRect)frame scaleFactor:(CGFloat)scale;
- (id)initWithFrame:(CGRect)frame;
- (id)initFromMainScreen;
// in here we will go through subviews and call onUnityUpdateViewLayout selector (if present)
// that allows to handle simple overlay child view layout without doing view controller magic
- (void)layoutSubviews;
- (void)recreateRenderingSurfaceIfNeeded;
- (void)recreateRenderingSurface;
// will match script-side Screen.orientation
@property (nonatomic, readonly) ScreenOrientation contentOrientation;
@end
@interface UnityView (Deprecated)
- (void)recreateGLESSurfaceIfNeeded __deprecated_msg("use recreateRenderingSurfaceIfNeeded instead.");
- (void)recreateGLESSurface __deprecated_msg("use recreateRenderingSurface instead.");
@end
#if PLATFORM_IOS
#include "UnityView+iOS.h"
#elif PLATFORM_TVOS
#include "UnityView+tvOS.h"
#endif
void ReportSafeAreaChangeForView(UIView* view);
// Computes safe area for a view in Unity coordinate system (origin of the view
// is bottom-left, as compared to standard top-left)
CGRect ComputeSafeArea(UIView* view);
@@ -0,0 +1,233 @@
#include "UnityView.h"
#include "UnityAppController.h"
#include "UnityAppController+Rendering.h"
#include "OrientationSupport.h"
#include "Unity/DisplayManager.h"
#include "Unity/UnityMetalSupport.h"
#include "Unity/ObjCRuntime.h"
extern bool _renderingInited;
extern bool _unityAppReady;
extern bool _skipPresent;
extern bool _supportsMSAA;
@implementation UnityView
{
CGSize _surfaceSize;
}
@synthesize contentOrientation = _curOrientation;
- (void)onUpdateSurfaceSize:(CGSize)size
{
_surfaceSize = size;
CGSize systemRenderSize = CGSizeMake(size.width * self.contentScaleFactor, size.height * self.contentScaleFactor);
_curOrientation = (ScreenOrientation)UnityReportResizeView(systemRenderSize.width, systemRenderSize.height, _curOrientation);
ReportSafeAreaChangeForView(self);
#if UNITY_CAN_USE_METAL
if (UnitySelectedRenderingAPI() == apiMetal)
((CAMetalLayer*)self.layer).drawableSize = systemRenderSize;
#endif
}
- (void)initImpl:(CGRect)frame scaleFactor:(CGFloat)scale
{
#if !PLATFORM_TVOS
self.multipleTouchEnabled = YES;
self.exclusiveTouch = YES;
#endif
self.contentScaleFactor = scale;
self.isAccessibilityElement = TRUE;
self.accessibilityTraits = UIAccessibilityTraitAllowsDirectInteraction;
#if UNITY_TVOS
_curOrientation = UNITY_TVOS_ORIENTATION;
#endif
[self onUpdateSurfaceSize: frame.size];
}
- (id)initWithFrame:(CGRect)frame scaleFactor:(CGFloat)scale;
{
if ((self = [super initWithFrame: frame]))
[self initImpl: frame scaleFactor: scale];
return self;
}
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame: frame]))
[self initImpl: frame scaleFactor: 1.0f];
return self;
}
- (id)initFromMainScreen
{
CGRect frame = [UIScreen mainScreen].bounds;
CGFloat scale = UnityScreenScaleFactor([UIScreen mainScreen]);
if ((self = [super initWithFrame: frame]))
[self initImpl: frame scaleFactor: scale];
return self;
}
- (void)layoutSubviews
{
if (_surfaceSize.width != self.bounds.size.width || _surfaceSize.height != self.bounds.size.height)
_shouldRecreateView = YES;
[self onUpdateSurfaceSize: self.bounds.size];
for (UIView* subView in self.subviews)
{
if ([subView respondsToSelector: @selector(onUnityUpdateViewLayout)])
[subView performSelector: @selector(onUnityUpdateViewLayout)];
}
[super layoutSubviews];
}
- (void)safeAreaInsetsDidChange
{
ReportSafeAreaChangeForView(self);
}
- (void)recreateRenderingSurfaceIfNeeded
{
unsigned requestedW, requestedH; UnityGetRenderingResolution(&requestedW, &requestedH);
int requestedMSAA = UnityGetDesiredMSAASampleCount(MSAA_DEFAULT_SAMPLE_COUNT);
int requestedSRGB = UnityGetSRGBRequested();
int requestedWideColor = UnityGetWideColorRequested();
int requestedMemorylessDepth = UnityMetalMemorylessDepth();
UnityDisplaySurfaceBase* surf = GetMainDisplaySurface();
if (_shouldRecreateView == YES
|| surf->targetW != requestedW || surf->targetH != requestedH
|| surf->disableDepthAndStencil != UnityDisableDepthAndStencilBuffers()
|| (_supportsMSAA && surf->msaaSamples != requestedMSAA)
|| surf->srgb != requestedSRGB
|| surf->wideColor != requestedWideColor
|| surf->memorylessDepth != requestedMemorylessDepth
)
{
[self recreateRenderingSurface];
}
}
- (void)recreateRenderingSurface
{
if (_renderingInited)
{
unsigned requestedW, requestedH;
UnityGetRenderingResolution(&requestedW, &requestedH);
RenderingSurfaceParams params =
{
.msaaSampleCount = UnityGetDesiredMSAASampleCount(MSAA_DEFAULT_SAMPLE_COUNT),
.renderW = (int)requestedW,
.renderH = (int)requestedH,
.srgb = UnityGetSRGBRequested(),
.wideColor = UnityGetWideColorRequested(),
.metalFramebufferOnly = UnityMetalFramebufferOnly(),
.metalMemorylessDepth = UnityMetalMemorylessDepth(),
.disableDepthAndStencil = UnityDisableDepthAndStencilBuffers(),
.useCVTextureCache = 0,
};
APP_CONTROLLER_RENDER_PLUGIN_METHOD_ARG(onBeforeMainDisplaySurfaceRecreate, &params);
[GetMainDisplay() recreateSurface: params];
// actually poke unity about updated back buffer and notify that extents were changed
UnityReportBackbufferChange(GetMainDisplaySurface()->unityColorBuffer, GetMainDisplaySurface()->unityDepthBuffer);
APP_CONTROLLER_RENDER_PLUGIN_METHOD(onAfterMainDisplaySurfaceRecreate);
if (_unityAppReady)
{
// seems like ios sometimes got confused about abrupt swap chain destroy
// draw 2 times to fill both buffers
// present only once to make sure correct image goes to CA
// if we are calling this from inside repaint, second draw and present will be done automatically
_skipPresent = true;
if (!UnityIsPaused())
{
UnityRepaint();
// we are not inside repaint so we need to draw second time ourselves
if (_viewIsRotating)
UnityRepaint();
}
_skipPresent = false;
}
}
_shouldRecreateView = NO;
}
@end
@implementation UnityView (Deprecated)
- (void)recreateGLESSurfaceIfNeeded { [self recreateRenderingSurfaceIfNeeded]; }
- (void)recreateGLESSurface { [self recreateRenderingSurface]; }
@end
static Class UnityRenderingView_LayerClassGLES(id self_, SEL _cmd)
{
return [CAEAGLLayer class];
}
static Class UnityRenderingView_LayerClassMTL(id self_, SEL _cmd)
{
return [[NSBundle bundleWithPath: @"/System/Library/Frameworks/QuartzCore.framework"] classNamed: @"CAMetalLayer"];
}
@implementation UnityRenderingView
+ (Class)layerClass
{
return nil;
}
+ (void)InitializeForAPI:(UnityRenderingAPI)api
{
IMP layerClassImpl = 0;
if (api == apiOpenGLES2 || api == apiOpenGLES3)
layerClassImpl = (IMP)UnityRenderingView_LayerClassGLES;
else if (api == apiMetal)
layerClassImpl = (IMP)UnityRenderingView_LayerClassMTL;
class_replaceMethod(object_getClass([UnityRenderingView class]), @selector(layerClass), layerClassImpl, UIView_LayerClass_Enc);
}
@end
void ReportSafeAreaChangeForView(UIView* view)
{
CGRect safeArea = ComputeSafeArea(view);
UnityReportSafeAreaChange(safeArea.origin.x, safeArea.origin.y,
safeArea.size.width, safeArea.size.height);
}
CGRect ComputeSafeArea(UIView* view)
{
CGSize screenSize = view.bounds.size;
CGRect screenRect = CGRectMake(0, 0, screenSize.width, screenSize.height);
UIEdgeInsets insets = UIEdgeInsetsMake(0, 0, 0, 0);
#if UNITY_HAS_IOSSDK_11_0 || UNITY_HAS_TVOSSDK_11_0
if (@available(iOS 11.0, tvOS 11.0, *))
insets = [view safeAreaInsets];
#endif
screenRect.origin.x += insets.left;
screenRect.origin.y += insets.bottom; // Unity uses bottom left as the origin
screenRect.size.width -= insets.left + insets.right;
screenRect.size.height -= insets.top + insets.bottom;
float scale = view.contentScaleFactor;
// Truncate safe area size because in some cases (for example when Display zoom is turned on)
// it might become larger than Screen.width/height which are returned as ints.
screenRect.origin.x = (unsigned)(screenRect.origin.x * scale);
screenRect.origin.y = (unsigned)(screenRect.origin.y * scale);
screenRect.size.width = (unsigned)(screenRect.size.width * scale);
screenRect.size.height = (unsigned)(screenRect.size.height * scale);
return screenRect;
}
@@ -0,0 +1,40 @@
#pragma once
@interface UnityViewControllerBase (iOS)
- (BOOL)shouldAutorotate;
- (BOOL)prefersStatusBarHidden;
- (UIStatusBarStyle)preferredStatusBarStyle;
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator;
@end
// for better handling of user-imposed screen orientation we will have specific ViewController implementations
// view controllers constrained to one orientation
@interface UnityPortraitOnlyViewController : UnityViewControllerBase
{
}
@end
@interface UnityPortraitUpsideDownOnlyViewController : UnityViewControllerBase
{
}
@end
@interface UnityLandscapeLeftOnlyViewController : UnityViewControllerBase
{
}
@end
@interface UnityLandscapeRightOnlyViewController : UnityViewControllerBase
{
}
@end
// this is default view controller implementation (autorotation enabled)
@interface UnityDefaultViewController : UnityViewControllerBase
{
}
@end
NSUInteger EnabledAutorotationInterfaceOrientations();
@@ -0,0 +1,190 @@
#if PLATFORM_IOS
#import "UnityViewControllerBase.h"
#import "UnityAppController.h"
#include "OrientationSupport.h"
#include "Keyboard.h"
#include "UnityView.h"
#include "PluginBase/UnityViewControllerListener.h"
#include "UnityAppController.h"
#include "UnityAppController+ViewHandling.h"
#include "Unity/ObjCRuntime.h"
// when returning from presenting UIViewController we might need to update app orientation to "correct" one, as we wont get rotation notification
@interface UnityAppController ()
- (void)updateAppOrientation:(UIInterfaceOrientation)orientation;
@end
@implementation UnityViewControllerBase (iOS)
- (BOOL)shouldAutorotate
{
return YES;
}
- (BOOL)prefersStatusBarHidden
{
static bool _PrefersStatusBarHidden = true;
static bool _PrefersStatusBarHiddenInited = false;
if (!_PrefersStatusBarHiddenInited)
{
NSNumber* hidden = [[[NSBundle mainBundle] infoDictionary] objectForKey: @"UIStatusBarHidden"];
_PrefersStatusBarHidden = hidden ? [hidden boolValue] : YES;
_PrefersStatusBarHiddenInited = true;
}
return _PrefersStatusBarHidden;
}
- (UIStatusBarStyle)preferredStatusBarStyle
{
static UIStatusBarStyle _PreferredStatusBarStyle = UIStatusBarStyleDefault;
static bool _PreferredStatusBarStyleInited = false;
if (!_PreferredStatusBarStyleInited)
{
NSString* style = [[[NSBundle mainBundle] infoDictionary] objectForKey: @"UIStatusBarStyle"];
if (style && [style isEqualToString: @"UIStatusBarStyleLightContent"])
_PreferredStatusBarStyle = UIStatusBarStyleLightContent;
_PreferredStatusBarStyleInited = true;
}
return _PreferredStatusBarStyle;
}
- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures
{
UIRectEdge res = UIRectEdgeNone;
if (UnityGetDeferSystemGesturesTopEdge())
res |= UIRectEdgeTop;
if (UnityGetDeferSystemGesturesBottomEdge())
res |= UIRectEdgeBottom;
if (UnityGetDeferSystemGesturesLeftEdge())
res |= UIRectEdgeLeft;
if (UnityGetDeferSystemGesturesRightEdge())
res |= UIRectEdgeRight;
return res;
}
- (BOOL)prefersHomeIndicatorAutoHidden
{
return UnityGetHideHomeButton();
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
ScreenOrientation curOrient = UIViewControllerOrientation(self);
ScreenOrientation newOrient = OrientationAfterTransform(curOrient, [coordinator targetTransform]);
// in case of presentation controller it will take control over orientations
// so to avoid crazy corner cases, make default view controller to ignore "wrong" orientations
// as they will come only in case of presentation view controller and will be reverted anyway
// NB: we still want to pass message to super, we just want to skip unity-specific magic
NSUInteger targetMask = 1 << ConvertToIosScreenOrientation(newOrient);
if (([self supportedInterfaceOrientations] & targetMask) != 0)
{
[UIView setAnimationsEnabled: UnityUseAnimatedAutorotation() ? YES : NO];
[KeyboardDelegate StartReorientation];
[GetAppController() interfaceWillChangeOrientationTo: ConvertToIosScreenOrientation(newOrient)];
[coordinator animateAlongsideTransition: nil completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
[self.view setNeedsLayout];
[GetAppController() interfaceDidChangeOrientationFrom: ConvertToIosScreenOrientation(curOrient)];
[KeyboardDelegate FinishReorientation];
[UIView setAnimationsEnabled: YES];
}];
}
[super viewWillTransitionToSize: size withTransitionCoordinator: coordinator];
}
@end
@implementation UnityDefaultViewController
- (NSUInteger)supportedInterfaceOrientations
{
NSAssert(UnityShouldAutorotate(), @"UnityDefaultViewController should be used only if unity is set to autorotate");
return EnabledAutorotationInterfaceOrientations();
}
@end
@implementation UnityPortraitOnlyViewController
- (NSUInteger)supportedInterfaceOrientations
{
return 1 << UIInterfaceOrientationPortrait;
}
- (void)viewWillAppear:(BOOL)animated
{
[GetAppController() updateAppOrientation: UIInterfaceOrientationPortrait];
[super viewWillAppear: animated];
}
@end
@implementation UnityPortraitUpsideDownOnlyViewController
- (NSUInteger)supportedInterfaceOrientations
{
return 1 << UIInterfaceOrientationPortraitUpsideDown;
}
- (void)viewWillAppear:(BOOL)animated
{
[GetAppController() updateAppOrientation: UIInterfaceOrientationPortraitUpsideDown];
[super viewWillAppear: animated];
}
@end
@implementation UnityLandscapeLeftOnlyViewController
- (NSUInteger)supportedInterfaceOrientations
{
return 1 << UIInterfaceOrientationLandscapeLeft;
}
- (void)viewWillAppear:(BOOL)animated
{
[GetAppController() updateAppOrientation: UIInterfaceOrientationLandscapeLeft];
[super viewWillAppear: animated];
}
@end
@implementation UnityLandscapeRightOnlyViewController
- (NSUInteger)supportedInterfaceOrientations
{
return 1 << UIInterfaceOrientationLandscapeRight;
}
- (void)viewWillAppear:(BOOL)animated
{
[GetAppController() updateAppOrientation: UIInterfaceOrientationLandscapeRight];
[super viewWillAppear: animated];
}
@end
NSUInteger EnabledAutorotationInterfaceOrientations()
{
NSUInteger ret = 0;
if (UnityIsOrientationEnabled(portrait))
ret |= (1 << UIInterfaceOrientationPortrait);
if (UnityIsOrientationEnabled(portraitUpsideDown))
ret |= (1 << UIInterfaceOrientationPortraitUpsideDown);
if (UnityIsOrientationEnabled(landscapeLeft))
ret |= (1 << UIInterfaceOrientationLandscapeRight);
if (UnityIsOrientationEnabled(landscapeRight))
ret |= (1 << UIInterfaceOrientationLandscapeLeft);
return ret;
}
#endif // PLATFORM_IOS
@@ -0,0 +1,7 @@
#pragma once
//for tvOS we need just one view controller subclass as there is no screen orientation here
@interface UnityDefaultViewController : UnityViewControllerBase
{
}
@end
@@ -0,0 +1,9 @@
#if PLATFORM_TVOS
#import "UnityViewControllerBase.h"
#import "UnityAppController.h"
@implementation UnityDefaultViewController
@end
#endif // PLATFORM_TVOS
@@ -0,0 +1,27 @@
#pragma once
#import <UIKit/UIKit.h>
#if PLATFORM_IOS
#define UNITY_VIEW_CONTROLLER_BASE_CLASS UIViewController
#elif PLATFORM_TVOS
#import <GameController/GCController.h>
#define UNITY_VIEW_CONTROLLER_BASE_CLASS GCEventViewController
#endif
@interface UnityViewControllerBase : UNITY_VIEW_CONTROLLER_BASE_CLASS
{
}
- (void)viewWillLayoutSubviews;
- (void)viewDidLayoutSubviews;
- (void)viewDidDisappear:(BOOL)animated;
- (void)viewWillDisappear:(BOOL)animated;
- (void)viewDidAppear:(BOOL)animated;
- (void)viewWillAppear:(BOOL)animated;
@end
#if PLATFORM_IOS
#include "UnityViewControllerBase+iOS.h"
#elif PLATFORM_TVOS
#include "UnityViewControllerBase+tvOS.h"
#endif
@@ -0,0 +1,44 @@
#import "UnityViewControllerBase.h"
#import "UnityAppController.h"
#import "UnityAppController+ViewHandling.h"
#import "PluginBase/UnityViewControllerListener.h"
@implementation UnityViewControllerBase
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
AppController_SendUnityViewControllerNotification(kUnityViewWillLayoutSubviews);
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
AppController_SendUnityViewControllerNotification(kUnityViewDidLayoutSubviews);
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear: animated];
AppController_SendUnityViewControllerNotification(kUnityViewDidDisappear);
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear: animated];
AppController_SendUnityViewControllerNotification(kUnityViewWillDisappear);
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear: animated];
AppController_SendUnityViewControllerNotification(kUnityViewDidAppear);
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear: animated];
AppController_SendUnityViewControllerNotification(kUnityViewWillAppear);
}
@end