iOS開發(fā)之UI篇(12)—— UIWindow

版本
Xcode 10.2
iPhone 6s (iOS12.4)
( 本文示例所用測試版本如上, 一些方法結(jié)論可能不適用于較舊版本的iOS/Xcode, 如需使用應(yīng)先測試驗證. )

目錄

UIWindow繼承
簡單結(jié)構(gòu)圖
簡介
一些常見的window
window的創(chuàng)建過程
屬性方法
應(yīng)用

繼承關(guān)系

UIWindow : UIView : UIResponder : NSObject

結(jié)構(gòu)

新建一個”Single View App”模板App, 點擊Debug view hierarchy按鈕, 打開的層級關(guān)系圖如下:

UIWindow簡單結(jié)構(gòu)

簡介

The backdrop for your app’s user interface and the object that dispatches events to your views.
編者譯: UIWindow對象充當了App中UI界面的背景(容器/載體), 還有一個作用是分派事件給各種view.

根據(jù)Apple官方文檔的這段話, 可以延伸為以下兩點:

  1. App中任何一個view, 只有添加到相應(yīng)的window中, 才能顯示出來;
  2. 觸摸事件會被傳遞到觸摸區(qū)域內(nèi)的最上層的window, 非觸摸事件會被傳遞到keyWindow (詳見后文), 并由window將事件分發(fā)給恰當?shù)膙iew.

一般來說, 一個App只有一個window. 當我們使用Single View App模板來創(chuàng)建App的時候, 系統(tǒng)會幫我們創(chuàng)建好一個Main.storyboard, 一個ViewController (關(guān)聯(lián)storyboard里面的VC), 以及一個AppDelegate等. 在AppDelegate.h文件中, 會有一個window屬性, 這個屬性的rootViewController就是前面的ViewController, 而且把ViewController的View添加到了window中, 這才把View顯示出來, 如上圖所示. 只不過這一系列的操作都是隱藏的, 所以一般我們很少和window打交道.
一個App只能有一個window嗎? 答案是否定的. App可以有很多window, 要么是我們自己創(chuàng)建的, 要么是系統(tǒng)幫我們創(chuàng)建的, 下文列舉一二.

App中有哪些常見window

  • 主window, 即AppDelegate.h里面的window, 由系統(tǒng)創(chuàng)建或者我們自己創(chuàng)建, 用來呈現(xiàn)App內(nèi)容;
  • 當使用UIAlertView或者UIAlertViewController創(chuàng)建一個提示彈框的時候, 系統(tǒng)會創(chuàng)建一個window: UITextEffectsWindow
  • 當彈出系統(tǒng)鍵盤的時候, 系統(tǒng)創(chuàng)建兩個window: UITextEffectsWindow 和 UIRemoteKeyboardWindow
  • 手機狀態(tài)欄的UIStatusBarWindow, 屬于系統(tǒng)級別, 不被App所持有;
  • 我們自己創(chuàng)建的一些window, 比如登錄界面, 加載界面, 自定義提示框, 自定義鍵盤, 懸浮球, 錄音狀態(tài)欄等等.
  • 外接屏幕需要新建一個window來顯示, 如投影到電視設(shè)備等.

注: 關(guān)于第2點, 使用UIAlertView創(chuàng)建提示框, 除了新增UITextEffectsWindow, 其實還有_UIAlertControllerShimPresenterWindow, 這貨變成了keyWindow, 不過不在App的windows列表中, 后文另有介紹.

window的創(chuàng)建

  • 系統(tǒng)模板創(chuàng)建

前文提到, 使用系統(tǒng)模板創(chuàng)建App, 系統(tǒng)會自動創(chuàng)建window. 大概流程是:

  1. 程序入口main(),
  2. 調(diào)用UIApplicationMain方法創(chuàng)建UIApplication對象(默認為AppDelegate),
  3. 根據(jù)Info.plist里”Main storyboard file base name” (等同targets中的Main interface選項) 對應(yīng)的名稱作為main storyboard并加載之,
  4. 偷偷摸摸地實例化AppDelegate.h中的window,
  5. 將Main.storyboard里的Initial View Controller (默認為ViewController)設(shè)為window的rootViewController, 此時相當于把ViewController的View添加到window中,
  6. 執(zhí)行makeKeyAndVisible方法將window設(shè)為keyWindow并使其可見(hidden=NO),
  7. 創(chuàng)建完成, 顯示.
  • 自定義創(chuàng)建

本來想新建一個空的project來從頭演示window的創(chuàng)建, 但是新版Xcode默認不允許創(chuàng)建空的項目. 無奈只好先創(chuàng)建模板App, 然后刪除默認storyboard/AppDelegate等文件, 再新建自定義的.

  1. 新建模板App;
  2. 刪除所有AppDelegate/storyboard/ViewController文件, 剩余Info.plist和main.m文件;
  3. Info.plist中把key”Launch screen interface file base name”與”Main storyboard file base name”后面的value去掉;
  4. 新建一個MyAppDelegate(名稱自定)繼承自UIResponder, 實現(xiàn)UIApplicationDelegate協(xié)議,
  5. 在MyAppDelegate.h文件中創(chuàng)建一個UIWindow實例對象myWindow, 使用強引用;
  6. 新建一個MyViewController繼承自UIViewController;
  7. 在MyAppDelegate.m中實現(xiàn)代理方法application:didFinishLaunchingWithOptions:, 返回值YES,
  8. 在main.m導(dǎo)入我們新建的MyAppDelegate.h, 并修改main函數(shù)里面的AppDelegate為MyAppDelegate;
  9. 最后在MyAppDelegate.m導(dǎo)入MyViewController.h, 在application:didFinishLaunchingWithOptions:方法中添加如下代碼.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    self.myWindow = [[UIWindow alloc] init];                // 實例化UIWindow, 其尺寸默認為屏幕大小([UIScreen mainScreen])
    MyViewController *VC = [[MyViewController alloc] init]; // 實例化MyViewController
    self.myWindow.rootViewController = VC;                  // 設(shè)置rootViewController, 相當于強引用VC, 故而VC不需用全局變量
    [self.myWindow makeKeyAndVisible];                      // 設(shè)為keyWindow并使其可見(Hidden=NO)
    
    return YES;
}

以上創(chuàng)建的工程和使用模板創(chuàng)建的工程對比, 除了不使用storyboard, 效果均一致.
如果要銷毀一個UIWindow對象, 可將其置nil即可 (至于網(wǎng)上也有人說先hidden再nil, 但我測試好像沒有這個必要). nil后, keyWindow會自動變?yōu)閇UIApplication sharedApplication].windows中l(wèi)evel高的并且是可見的window, 如果level相同, 后添加的將成為新keyWindow; 如果不符合前面的條件, 則keyWindow值為nil.
到這里, 我們應(yīng)該對UIWindow有了個簡單的認識, 接下來再探討一下UIWIndow的一些方法屬性.

方法屬性

@property(nonatomic,strong) UIScreen *screen NS_AVAILABLE_IOS(3_2);  // default is [UIScreen mainScreen]. changing the screen may be an expensive operation and should not be done in performance-sensitive code

@property(nonatomic) UIWindowLevel windowLevel;                   // default = 0.0
@property(nonatomic,readonly,getter=isKeyWindow) BOOL keyWindow;
- (void)becomeKeyWindow;                               // override point for subclass. Do not call directly
- (void)resignKeyWindow;                               // override point for subclass. Do not call directly

- (void)makeKeyWindow;
- (void)makeKeyAndVisible;                             // convenience. most apps call this to show the main window and also make it key. otherwise use view hidden property

@property(nullable, nonatomic,strong) UIViewController *rootViewController NS_AVAILABLE_IOS(4_0);  // default is nil

- (void)sendEvent:(UIEvent *)event;                    // called by UIApplication to dispatch events to views inside the window

- (CGPoint)convertPoint:(CGPoint)point toWindow:(nullable UIWindow *)window;    // can be used to convert to another window
- (CGPoint)convertPoint:(CGPoint)point fromWindow:(nullable UIWindow *)window;  // pass in nil to mean screen
- (CGRect)convertRect:(CGRect)rect toWindow:(nullable UIWindow *)window;
- (CGRect)convertRect:(CGRect)rect fromWindow:(nullable UIWindow *)window;

這些方法屬性Apple基本都有注解, 下面挑重點一一探討.

1. screen

默認為[UIScreen mainScreen] (屏幕大小). 我們也可自定義一個尺寸, 但是假如不鋪滿屏幕, 尺寸之外的區(qū)域?qū)@示不出來, 呈黑色.

2. windowLevel

window層級, 表示在z軸方向上的位置關(guān)系. 屬于CGFloat類型, 默認值為0.0, 取值范圍為-10000000.0到10000000.0. 驗證代碼如下:

self.myWindow.windowLevel = -100000000000000.0;
NSLog(@"%f", self.myWindow.windowLevel);        // -10000000.000000
self.myWindow.windowLevel = 100000000000000.0;
NSLog(@"%f", self.myWindow.windowLevel);        // 10000000.000000

另外, 系統(tǒng)定義了幾個層級, 其值如下:

UIKIT_EXTERN const UIWindowLevel UIWindowLevelNormal;       // 0.0
UIKIT_EXTERN const UIWindowLevel UIWindowLevelAlert;        // 2000.0
UIKIT_EXTERN const UIWindowLevel UIWindowLevelStatusBar;    // 1000.0

在可見狀態(tài)下, 層級高的window會遮擋掉層級低的window. 例如: 設(shè)置myWindow.windowLevel=999.9, 狀態(tài)欄可見; myWindow.windowLevel=1000.1, 狀態(tài)欄被覆蓋不可見.
如果兩個window的windowLevel相等, 那么后顯示出來的window會覆蓋前面的, 所謂顯示, 指的是self.myWindow.hidden=No或[self.myWindow makeKeyAndVisible]操作.

3. keyWindow

The key window receives keyboard and other non-touch related events. Only one window at a time may be the key window.

這個屬性相當重要, 因為一個window只有成為keyWindow才能接收鍵盤事件和非觸摸類事件. 這里所說的鍵盤事件, 并不是指鍵盤的彈出收起事件, 因為這些事件任何一個類只要注冊通知都能接收到, 這里說的鍵盤事件應(yīng)該指的是鍵盤傳遞的值. 例如, 創(chuàng)建兩個window同時顯示, 每個window上的view里面添加一個textField用于彈出鍵盤, 當點擊其中一個textField后, 其所在的window就會被設(shè)置成keyWindow, 然后才可愉快地輸入.
同一時刻只能有一個keyWindow.
這里順便提一下, 假如一開始我們沒設(shè)置makeKeyAndVisible, 而只是hidden=No, 此時window仍可顯示, 但是keyWindow為nil, 如果在界面上面添加textField, 點擊后系統(tǒng)就會將當前的window設(shè)為keyWindow.
這個屬性是readonly只讀屬性, 如果要設(shè)置一個window為keyWindow, 使用makeKeyWindow或者makeKeyAndVisible方法.

4. becomeKeyWindow

當window成為keyWindow時, 系統(tǒng)會調(diào)用此方法同時發(fā)出UIWindowDidBecomeKeyNotification通知, 以便該window知道自己成為了keyWindow. 需要注意的是, 這是類似一個系統(tǒng)回調(diào)方法, 我們不要主動調(diào)用他, 否則可能出現(xiàn)不可預(yù)料后果.
我們可以重寫它, 來執(zhí)行成為keyWindow后的相關(guān)任務(wù).

5. resignKeyWindow

當window從keyWindow變成非KeyWindow時, 系統(tǒng)會調(diào)用此方法同時發(fā)出UIWindowDidResignKeyNotification通知. 同上.

6. makeKeyWindow

調(diào)用此方法的window將成為新的keyWindow, 同時不改變其可見性(hidden). 強調(diào), 同一時刻只能有一個keyWindow.

7. makeKeyAndVisible

調(diào)用此方法的window將成為新的keyWindow, 同時可見(hidden=NO). 相當于 makeKeyWindow + hidden=NO.

8. rootViewController

根視圖控制器, 提供窗口的內(nèi)容視圖, 即rootViewController的self.view當做window的內(nèi)容視圖來展示, 其view跟隨window的大小的變化而變化.

9. sendEvent:

UIApplication對象調(diào)用這個方法分派事件給window, 然后window又將這些事件分派給適當?shù)膙iew (UIApplication和UIWindow均聲明了sendEvent:方法). 我們可以自定義子類繼承自UIApplication/UIWindow, 并重寫這個方法, 將事件分派給UIResponder的響應(yīng)程序鏈, 實現(xiàn)對事件的監(jiān)控或執(zhí)行特殊的事件處理.

10. convertPoint..

坐標轉(zhuǎn)換. 略.

window變化通知

UIKIT_EXTERN NSNotificationName const UIWindowDidBecomeVisibleNotification; // 當window顯示(hidden=NO)
UIKIT_EXTERN NSNotificationName const UIWindowDidBecomeHiddenNotification;  // 當window隱藏(hidden=YES)
UIKIT_EXTERN NSNotificationName const UIWindowDidBecomeKeyNotification;     // 當window成為keyWindow
UIKIT_EXTERN NSNotificationName const UIWindowDidResignKeyNotification;     // 當window變成非keyWindow

注意:

  1. 如果當前keyWindow直接nil, 不會發(fā)送UIWindowDidResignKeyNotification通知; 而只有別的window調(diào)用makeKeyWindow/makeKeyAndVisible后, 才會發(fā)送UIWindowDidResignKeyNotification通知. 例如: 有兩個window A和B, A為keyWindow, 當直接window A = nil, 則keyWindow變成window B, 但此時不會發(fā)送通知; 而A為keyWindow, B調(diào)用makeKeyWindow/makeKeyAndVisible后, 則B也會變成keyWindow, 同時發(fā)送通知.
  2. 如果對當前keyWindow直接隱藏(hidden=YES), 首先會發(fā)送UIWindowDidResignKeyNotification通知并且辭去keyWindow職務(wù), 接著發(fā)送UIWindowDidBecomeHiddenNotification通知.

應(yīng)用

  • UIAlertView

這個類在iOS9.0中被遺棄, 但目前還可使用. 當我們show這個類的實例 (彈出提示框) 的時候, 系統(tǒng)實際上先后創(chuàng)建了兩個window: _UIAlertControllerShimPresenterWindowUITextEffectsWindow.

show流程是:

  1. _UIAlertControllerShimPresenterWindow可見
  2. 當前keyWindow變成非keyWindow
  3. _UIAlertControllerShimPresenterWindow成為keyWindow
  4. UITextEffectsWindow可見

dismiss流程是:

  1. _UIAlertControllerShimPresenterWindow變成非keyWindow
  2. 原來的window成為keyWindow
  3. _UIAlertControllerShimPresenterWindow隱藏

在alertView出來的時候, keyWindow是_UIAlertControllerShimPresenterWindow; alertView消失后, windows中仍保留UITextEffectsWindow.

_UIAlertControllerShimPresenterWindow是什么?
暫時查不到資料, 但根據(jù)字面意思, 是alaerView的呈現(xiàn)載體, 也就是說alertView在_UIAlertControllerShimPresenterWindow里面, 而后者不被App持有.

UITextEffectsWindow又是什么鬼?
也沒啥資料, 大概是和鍵盤輸入相關(guān)的window. (alertView的按鈕不就相當于鍵盤嘛..)

  • UIAlertController

iOS9.0之后, 系統(tǒng)建議我們使用的類. 但是和UIAlertView不同的是, 使用UIAlertController彈出彈框后, 雖然增加了UITextEffectsWindow, 但是keyWindow并沒有改變, 而且UIAlertController顯示的view是添加到keyWindow去顯示的.

  • keyboard

iOS10.0之前, 調(diào)用系統(tǒng)鍵盤后, 出現(xiàn)UITextEffectsWindow; iOS10.0之后, 新增了UIRemoteKeyboardWindow. UITextEffectsWindow是和鍵盤輸入相關(guān)的window
UIRemoteKeyboardWindow是鍵盤視圖所在的window, 而且level為最高的10000000.0

  • 重登陸

當我們使用支付寶或者一些金融類App的時候, 會發(fā)現(xiàn)當App從后臺返回前臺的時候, 總是要重新輸入密碼, 以提高軟件安全性. 這個重新輸入的密碼界面一般是通過新增一個window來實現(xiàn)的, 因為它可能從App中任意一個界面調(diào)用出來, 使用VC或者view的話終究不太方便.
下面來簡單介紹一下實現(xiàn)過程, 代碼比較簡單, 就不貼上了:

  1. AppDelegate中監(jiān)聽返回前臺的通知applicationWillEnterForegroundNotification;
  2. 在通知中實例化一個自定義window并makeKeyAndVisible, window中加入我們的密碼界面;
  3. 當用戶輸入正確密碼后直接將該window=nil, 系統(tǒng)自動將keyWindow變?yōu)樯现暗膚indow.
  • 懸浮球

只是簡單實現(xiàn), 拒絕和那些成熟的項目作對比.
工程結(jié)構(gòu)如下:


懸浮球Demo

在模板App上面添加了兩個VC, 分別作為suspensionWindow和logWindow的rootViewController. 下面直接貼上代碼.

AppDelegate.h

#import <UIKit/UIKit.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;
@property (nonatomic, strong) UIWindow *suspensionWindow;
@property (nonatomic, strong) UIWindow *logWindow;

@end

AppDelegate.m

#import "AppDelegate.h"
#import "SuspensionViewController.h"
#import "LogViewController.h"

@interface AppDelegate () <SuspensionViewControllerDelegate>

@end

@implementation AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    // 創(chuàng)建suspensionWindow
    self.suspensionWindow = [[UIWindow alloc] initWithFrame:CGRectMake(0, 100, 60, 60)];
    SuspensionViewController *suspensionVC = [[SuspensionViewController alloc] init];
    suspensionVC.delegate = self;
    self.suspensionWindow.rootViewController = suspensionVC;
    self.suspensionWindow.windowLevel = 0.2;        // 主window 0.0, 鍵盤window 1.0, logWindow 0.1
    self.suspensionWindow.hidden = NO;              // 可見
    
    return YES;
}


#pragma makr - SuspensionViewControllerDelegate

// 單擊懸浮球 回調(diào)
- (void)suspensionViewController:(SuspensionViewController *)suspensionVC didClickButton:(UIButton *)btn {
    
    if (btn.selected) {
        
        // 創(chuàng)建logWindow并設(shè)為可見
        self.logWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
        LogViewController *logVC = [[LogViewController alloc] init];
        self.logWindow.rootViewController = logVC;
        self.logWindow.windowLevel = 0.1;
        self.logWindow.hidden = NO;
        
    }else {
        
        // 銷毀logWindow
        self.logWindow = nil;
        
    }
}


@end

ViewController.m

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timeIsUp:) userInfo:nil repeats:YES];
}


- (void)timeIsUp:(NSTimer *)timer {
    
    NSString *message = [NSString stringWithFormat:@"%@ test", [self getCurrentTimeString]];
    [[NSNotificationCenter defaultCenter] postNotificationName:@"LogMessageNotification" object:message];
}


// 獲取當前時間str
- (NSString *)getCurrentTimeString {

    // 日期解析器
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.timeZone = [NSTimeZone systemTimeZone];   // 設(shè)置時區(qū) (跟隨系統(tǒng))
    [dateFormatter setDateFormat:@"hh:mm:ss:SSS"];          // 設(shè)置時間字符串格式
    
    // 獲取當前時間(GMT)
    NSDate *date = [NSDate date];
    
    // 轉(zhuǎn)換成時間字符串
    NSString *dateStr = [dateFormatter stringFromDate:date];
    
    return dateStr;
}


@end

SuspensionViewController.h

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@class SuspensionViewController;

@protocol SuspensionViewControllerDelegate <NSObject>

@optional
- (void)suspensionViewController:(SuspensionViewController *)suspensionVC didClickButton:(UIButton *)btn;

@end


@interface SuspensionViewController : UIViewController

@property (nonatomic, weak) id<SuspensionViewControllerDelegate>    delegate;

@end

NS_ASSUME_NONNULL_END

SuspensionViewController.m

#import "SuspensionViewController.h"
#import "AppDelegate.h"


@interface SuspensionViewController ()

@property (nonatomic, strong) UIButton  *btn;

@end


@implementation SuspensionViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // init UI
    self.view.backgroundColor = [UIColor purpleColor];
    self.view.alpha = 0.5;
    [self.view addSubview:self.btn];
    
    // 添加拖動手勢
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panAction:)];
    [self.view addGestureRecognizer:pan];
}


- (void)viewWillLayoutSubviews {
    NSLog(@"%s", __func__);
    
    self.view.layer.cornerRadius = self.view.frame.size.width/2.0;
    self.btn.frame = self.view.bounds;
}


#pragma mark - UI事件

- (void)btnAction:(UIButton *)btn {
    btn.selected = !btn.selected;
    
    if ([self.delegate respondsToSelector:@selector(suspensionViewController:didClickButton:)]) {
        [self.delegate suspensionViewController:self didClickButton:btn];
    }
}


- (void)panAction:(UIPanGestureRecognizer *)sender {
    
    // 獲取AppDelegate實例, 主window, 懸浮球window
    AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
    UIWindow *mainWindow = appDelegate.window;
    UIWindow *suspensionWindow = appDelegate.suspensionWindow;
    
    switch (sender.state) {
            
        case UIGestureRecognizerStateBegan:
            {
                self.view.alpha = 1.0;
            }
            break;
            
        case UIGestureRecognizerStateChanged:
            {
                // 懸浮球跟隨手勢移動
                suspensionWindow.center = [sender locationInView:mainWindow];
            }
            break;
            
        case UIGestureRecognizerStateEnded:
        case UIGestureRecognizerStateFailed:
        case UIGestureRecognizerStateCancelled:
            {
                self.view.alpha = 0.5;
                // 懸浮球靠邊
                float x = suspensionWindow.frame.size.width/2;
                if (suspensionWindow.center.x > mainWindow.frame.size.width/2) {
                    x = mainWindow.frame.size.width - suspensionWindow.frame.size.width/2;
                }
                [UIView animateWithDuration:0.2 animations:^{
                    suspensionWindow.center = CGPointMake(x, suspensionWindow.center.y);
                }];
            }
            break;
            
        default:
            break;
    }
}


#pragma mark - lazy

- (UIButton *)btn {
    
    if (_btn == nil) {
        _btn = [UIButton buttonWithType:UIButtonTypeCustom];
        _btn.frame = self.view.bounds;
        [_btn setTitle:@"Log" forState:UIControlStateNormal];
        [_btn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
        [_btn addTarget:self action:@selector(btnAction:) forControlEvents:UIControlEventTouchUpInside];
    }
    
    return _btn;
}


@end

LogViewController.m

#import "LogViewController.h"

@interface LogViewController ()

@property (nonatomic, strong) UITextView    *textView;

@end

@implementation LogViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // init UI
    self.view.backgroundColor = [UIColor colorWithRed:177.0/255.0 green:177.0/255.0 blue:177.0/255.0 alpha:0.5];
    [self.view addSubview:self.textView];
    
    // 監(jiān)聽通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(logMessage:) name:@"LogMessageNotification" object:nil];
}


- (void)dealloc
{
    // 移除所有通知
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}


// 通知響應(yīng)方法
- (void)logMessage:(NSNotification *)sender {
    
    NSString *message = (NSString *)sender.object;
    
    dispatch_async(dispatch_get_main_queue(), ^{
        self.textView.text = [NSString stringWithFormat:@"%@%@\n", self.textView.text, message];
        // 總是跳到最后一行
        [self.textView scrollRangeToVisible:NSMakeRange(self.textView.text.length, 1)];
    });
}


#pragma mark - lazy

- (UITextView *)textView {
    
    if (_textView == nil) {
        _textView = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width/2, [UIScreen mainScreen].bounds.size.height/2)];
        _textView.center = CGPointMake([UIScreen mainScreen].bounds.size.width/2, [UIScreen mainScreen].bounds.size.height/2);
        _textView.backgroundColor = [UIColor colorWithRed:0 green:1.0 blue:1.0 alpha:0.5];
        _textView.editable = NO;    // 禁用鍵盤
        _textView.text = @"";
    }
    
    return _textView;
}


@end

運行效果如下:


LearnUIWindow
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蹂风,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子田篇,更是在濱河造成了極大的恐慌揽乱,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,744評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件允蜈,死亡現(xiàn)場離奇詭異叔收,居然都是意外死亡,警方通過查閱死者的電腦和手機痢士,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來茂装,“玉大人怠蹂,你說我怎么就攤上這事∩偬” “怎么了城侧?”我有些...
    開封第一講書人閱讀 163,105評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長彼妻。 經(jīng)常有香客問我嫌佑,道長,這世上最難降的妖魔是什么侨歉? 我笑而不...
    開封第一講書人閱讀 58,242評論 1 292
  • 正文 為了忘掉前任屋摇,我火速辦了婚禮,結(jié)果婚禮上幽邓,老公的妹妹穿的比我還像新娘炮温。我一直安慰自己,他們只是感情好牵舵,可當我...
    茶點故事閱讀 67,269評論 6 389
  • 文/花漫 我一把揭開白布柒啤。 她就那樣靜靜地躺著,像睡著了一般畸颅。 火紅的嫁衣襯著肌膚如雪担巩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,215評論 1 299
  • 那天没炒,我揣著相機與錄音涛癌,去河邊找鬼。 笑死送火,一個胖子當著我的面吹牛拳话,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播漾脂,決...
    沈念sama閱讀 40,096評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼假颇,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了骨稿?” 一聲冷哼從身側(cè)響起笨鸡,我...
    開封第一講書人閱讀 38,939評論 0 274
  • 序言:老撾萬榮一對情侶失蹤姜钳,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后形耗,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體哥桥,經(jīng)...
    沈念sama閱讀 45,354評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,573評論 2 333
  • 正文 我和宋清朗相戀三年激涤,在試婚紗的時候發(fā)現(xiàn)自己被綠了拟糕。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,745評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡倦踢,死狀恐怖送滞,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情辱挥,我是刑警寧澤犁嗅,帶...
    沈念sama閱讀 35,448評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站晤碘,受9級特大地震影響褂微,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜园爷,卻給世界環(huán)境...
    茶點故事閱讀 41,048評論 3 327
  • 文/蒙蒙 一宠蚂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧童社,春花似錦求厕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽旅东。三九已至灭抑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間抵代,已是汗流浹背腾节。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留荤牍,地道東北人案腺。 一個月前我還...
    沈念sama閱讀 47,776評論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像康吵,于是被迫代替她去往敵國和親劈榨。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,652評論 2 354

推薦閱讀更多精彩內(nèi)容