2014年2月17日 星期一

iOS 7上portrait landscape要如何在不同分頁上,做不同的設定。

為了這個螢幕轉方向的問題,弄了好久才找到最佳解法,之前用了一個比較蠢的解法,就是設定一個事件去監聽轉換的方向,然後開始針對不同版面進行為整形....一個版面刻了兩個小時,就是為了橫向直向都要可以,一整個累.....如果有需求的,可以參考一下這個方法,雖然我覺得沒有很好,請注意這邊的做法確定在ios6、ios7上確實可行,但在ios5以下版本,我就不敢確定了,我就先來說明這個方法。

Step1:宣告一個監聽事件,監聽UIDeviceOrientationDidChangeNotification方向轉換


[[NSNotificationCenter defaultCenter] addObserver:self
                                          selector:@selector(orientationChanged:)
                                    name:UIDeviceOrientationDidChangeNotification
                                               object:nil];

Step2:撰寫function orientationChanged,針對不同規格刻版面。

- (void)orientationChanged:(NSNotification *)notification
{
    CGFloat img_x=0.0f,img_y=65.0f,img_width=320.0f,img_height=40.0f;
    CGFloat Seg_x=10.0f,Seg_y=70.0f,Seg_width=300.0f,Seg_height=30.0f;
    UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation;
    if ( UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad ){
         NSLog(@"ipad");
        if (UIDeviceOrientationIsLandscape(deviceOrientation))
        {
            //do something is Landscape
            img_x=0.0f,img_y=65.0f,img_width=1024.0f,img_height=50.0f;
            Seg_x=253.0f,Seg_y=75.0f,Seg_width=500.0f,Seg_height=30.0f;
            if (debug) NSLog(@"UIDeviceOrientationIsLandscape");
            imageView.frame=CGRectMake(img_x,img_y,img_width,img_height);
            mySegmentedControl.frame=CGRectMake(Seg_x,Seg_y,Seg_width,Seg_height);
        }
        else if (UIDeviceOrientationIsPortrait(deviceOrientation))
        {
            //do something is Portrait
            img_x=0.0f,img_y=65.0f,img_width=768.0f,img_height=50.0f;
            Seg_x=130.0f,Seg_y=75.0f,Seg_width=500.0f,Seg_height=30.0f;
            if (debug) NSLog(@"UIDeviceOrientationIsPortrait");
            imageView.frame=CGRectMake(img_x,img_y,img_width,img_height);
            mySegmentedControl.frame=CGRectMake(Seg_x,Seg_y,Seg_width,Seg_height);
        }
    }else if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone){
        NSLog(@"iphone");
        if (UIDeviceOrientationIsLandscape(deviceOrientation))
        {
            //do something is Landscape
            img_x=0.0f,img_y=53.5f,img_width=568.0f,img_height=40.0f;
            Seg_x=138.0f,Seg_y=60.0f,Seg_width=300.0f,Seg_height=30.0f;
            if (debug) NSLog(@"UIDeviceOrientationIsLandscape");
            imageView.frame=CGRectMake(img_x,img_y,img_width,img_height);
            mySegmentedControl.frame=CGRectMake(Seg_x,Seg_y,Seg_width,Seg_height);
        }
        else if (UIDeviceOrientationIsPortrait(deviceOrientation))
        {
            //do something is Portrait
            img_x=0.0f,img_y=65.5f,img_width=320.0f,img_height=40.0f;
            Seg_x=10.0f,Seg_y=70.0f,Seg_width=300.0f,Seg_height=30.0f;
            if (debug) NSLog(@"UIDeviceOrientationIsPortrait");
            if (deviceOrientation==UIDeviceOrientationPortraitUpsideDown){
                img_x=0.0f,img_y=53.5f,img_width=568.0f,img_height=40.0f;
                Seg_x=138.0f,Seg_y=60.0f,Seg_width=300.0f,Seg_height=30.0f;
            }
            imageView.frame=CGRectMake(img_x,img_y,img_width,img_height);
            mySegmentedControl.frame=CGRectMake(Seg_x,Seg_y,Seg_width,Seg_height);
        }
    }
    
}

另外一種做法,可以說是比較懶惰的做法,只要刻一個版面就好,可以針對不同需求來作設定。
我想大家都應該知道ios6以上針對版面的轉換,已經更新了函式,現在是

- (BOOL)shouldAutorotate
{
    //returns true if want to allow orientation change
    return NO;
}
- (NSUInteger)supportedInterfaceOrientations
{
    //decide number of origination tob supported by Viewcontroller.
    return UIInterfaceOrientationMaskAll;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
    //from here you Should try to Preferred orientation for ViewController
    return UIInterfaceOrientationPortrait;
}
但是呢?如果你用stroyboard來做版面的時候,會發現,似乎沒辦法針對每個版面進行不同的設定,例如你要再navigation controller 將 portrait 轉成 landscape,這個意思是第一個版面是portrait,第二個頁面是landscape,這樣就會遇到了困難,這樣怎麼解決呢?

Step1:在AppDelegate內的@end後加入下面程式碼


@implementation UINavigationController (Rotation_IOS6)

-(BOOL)shouldAutorotate
{
    //NSLog(@"shouldAutorotate");
    return [[self.viewControllers lastObject] shouldAutorotate];
}

-(NSUInteger)supportedInterfaceOrientations
{
    //NSLog(@"supportedInterfaceOrientations");
    return [[self.viewControllers lastObject] supportedInterfaceOrientations];
}

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
    //NSLog(@"preferredInterfaceOrientationForPresentation");
    return [[self.viewControllers lastObject] preferredInterfaceOrientationForPresentation];
}
@end

Step2:在你要的頁面中加入上述的函式即可

- (BOOL)shouldAutorotate
{
    //returns true if want to allow orientation change
    return NO;
}
- (NSUInteger)supportedInterfaceOrientations
{
    //decide number of origination tob supported by Viewcontroller.
    return UIInterfaceOrientationMaskAll;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
    //from here you Should try to Preferred orientation for ViewController
    return UIInterfaceOrientationPortrait;
}






2013年12月26日 星期四

[android]AudioTrack使用,用來完成PCM Buffer To Play,並提供Play、Pause、Resume與Stop的說明。

近期,在研究如何用播放buffer PCM,作一個底層的播放,自己控制,不透過MediaPlayer class來控制這些流程,因為工作需要,所以就不說明了,重點放在怎麼做這樣的事情。

1.先init()這個AudioTrack,一開始它會要求你這個AudioTrack的buffer大小,這個取決於你的應用,另外重要的是AudioTrack.MODE_STATIC,這個是說明你這個buffer是一次寫入還是分批寫入,如果你是一次寫入的話那就用MODE_STATIC,分批寫入的話就用MODE_STREAM,但是兩邊的用法不一樣,如果你是想要用MODE_STREAM來完成這樣的事情,那我這篇內文就不適用了。

2.playAudioTrack(short[] data, int offset, int length),這就是播放的function,不過因為我這邊使用的是MODE_STATIC mode,所以offset在這裡都不適用,因為不需要向後加data,所以就只要提供data buf 跟data size即可,注意編碼問題(big little endian),如果你的測試音檔從windows上copy過來,請注意它的編碼,會跟Android不一樣。

3.playpause(),這個就是]pause與resume的function,沒啥好說的,大致上就是按下pause,記住你現在的位置(getPlaybackHeadPosition),resume播放這個位置(setPlaybackHeadPosition)。

4.playstop(),很奇怪的設計,要先pause()後,才可以stop(),直接stop沒作用,似乎下stop指令只有改PLAYSTATE_STOPPED為1而已,要清楚還是要去看他這邊stop後做了哪些事情,我是沒再去看這邊的內容。

5.如果你需要Playback的話,請記得加上OnPlaybackPositionUpdateListener 它會提供兩個function給你。
onMarkerReached:大概的意思當你設定位置到了,它會通知你,透過setNotificationMarkerPosition來設定,這大概的想法就是拿來做播放完畢的通知,通知UI層核時播放完畢

onPeriodicNotification:大概的意思當你設定位置的n倍就會通知你,例如你設定frame=1000,那它1000、2000、3000....都會通知你。

6.這個範例大致上說明了怎麼使用,但不完整,但是該點到的都有點到,但是沒包裝。

AndroidManifest.xml 開權限:
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>

Source Code:
public class JWavePlayer implements OnPlaybackPositionUpdateListener {
private static final boolean DBG = true;
private static final String TAG = "JWavePlayer";

private int audioFrequency;
private int audioChannel; // 8k or 16k
private int audioEncoding; // Mono or STEREO
private int audioTrackSize;// 288000/16000=18(s) 每一個TrackSize容納18秒
AudioTrack mAudioTrack; // 8bit or 16bit

public short[] g_data;
public int g_offset;
public int g_length;
public int g_frame;
public JWavePlayer() {
audioFrequency = 16000;
audioChannel = AudioFormat.CHANNEL_OUT_MONO;
audioEncoding = AudioFormat.ENCODING_PCM_16BIT;
audioTrackSize=288000;
}
public void init() {
if (mAudioTrack != null) {
release();
}
int minBufSize = AudioTrack.getMinBufferSize(audioFrequency,
audioChannel, audioEncoding);
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, audioFrequency,
audioChannel, audioEncoding, audioTrackSize, AudioTrack.MODE_STATIC);
mAudioTrack.setPlaybackPositionUpdateListener(this);
}

public void playAudioTrack(short[] data, int offset, int length) {
if (data == null || data.length == 0 && mAudioTrack.getPlayState()==mAudioTrack.PLAYSTATE_STOPPED) {
return;
}
try {

mAudioTrack.reloadStaticData();
mAudioTrack.write(data, offset, length);
mAudioTrack.setNotificationMarkerPosition(length/2);
mAudioTrack.play();
} catch (Exception e) {
// TODO: handle exception
Log.i("MyAudioTrack", "catch exception..."+e.getMessage());
}
}
public void playpause(){
try {
if (mAudioTrack.getPlayState()==mAudioTrack.PLAYSTATE_PLAYING){
g_frame=mAudioTrack.getPlaybackHeadPosition();
mAudioTrack.pause();
}else if (mAudioTrack.getPlayState()==mAudioTrack.PLAYSTATE_PAUSED){
mAudioTrack.setPlaybackHeadPosition(g_frame);
mAudioTrack.play();
}
} catch (Exception e) {
Log.i("MyAudioTrack", "catch exception..."+e.getMessage());
}
Log.e("MyAudioTrack",String.valueOf(mAudioTrack.getPlayState()) );
}
public void playStop(){
mAudioTrack.pause();
mAudioTrack.stop();
Log.e("MyAudioTrack",String.valueOf(mAudioTrack.getPlayState()) );
}
public void state(){
Log.d(TAG,"getPlayState:"+mAudioTrack.getPlayState());
//PLAYSTATE_STOPPED 1
//PLAYSTATE_PAUSED 2
//PLAYSTATE_PLAYING 3
Log.d(TAG,"getState:"+mAudioTrack.getState());
//STATE_UNINITIALIZED 0
//STATE_INITIALIZED 1
//STATE_NO_STATIC_DATA 2
//Log.d(TAG,"getStreamType:"+mAudioTrack.getStreamType());
}

public void release() {
if (mAudioTrack != null) {
mAudioTrack.stop();
mAudioTrack.release();
}
}
@Override
public void onMarkerReached(AudioTrack track) {
// TODO Auto-generated method stub
Log.e(TAG,"MarkerReached State:"+track.getState());

}
@Override
public void onPeriodicNotification(AudioTrack track) {
// TODO Auto-generated method stub
Log.e(TAG,"PeriodicNotification State:"+track.getState());
}

}

2013年7月4日 星期四

Facebook SDK install some Problem

從facebook sdk上使用時所遇到的一些流程與問題紀錄
官方說明文件:https://developers.facebook.com/docs/getting-started/facebook-sdk-for-android/3.0/

1.Install the Prerequisites
You need Eclipse, the Android SDK, and the ADT Plugin
   就是安裝Eclipse起來,可以使用Android Project就可以。
2.Install the Facebook SDK for Android and the Facebook APK
Download and extract the installation package. Install the Facebook app to your emulator
    下載Facebook SDK並安裝Facebook.apk
3.Import the SDK into Eclipse
Add the SDK as a library project to your workspace, along with the sample projects
   就是import facebook sdk(其他附屬的sample可以不用import 無所謂)
    這邊要注意的就是java compiler版本

4.Run the Samples
Try the sample projects, and add your Android key hash to your Facebook profile
   第一個Run的Sample是這個
  • ProfilePictureSample: demonstrates non-logged-in usage of the profile picture control
原因是這個只是測試facebook是否安裝完成其他設定都不用
   這邊的重點是Library的Add..因為位置不同,記得要重新add
   再來就是import android.support.v4的問題...記得要Add External JARs,這個檔案在Facebook SDK/libs裡面,有些人會copy到自己的project的libs裡面,應該都是可以的。

   先確定第一個sample可以Run 就可以進行下一個步驟了

第二個要測試的AP是
  • HelloFacebookSample: an all-round sample that demonstrates profile access, status updates and photo uploading
主要是要測試Android hash key是否正確,首先要在命令列下打(mac os)
keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore | openssl sha1 -binary | openssl base64
切忌不要輸入密碼,直接按Enter既可,會得到一組碼,然後再oncreate中加入下面的code:
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Add code to print out the key hash
    try {
        PackageInfo info = getPackageManager().getPackageInfo(
                "com.facebook.samples.hellofacebook", 
                PackageManager.GET_SIGNATURES);
        for (Signature signature : info.signatures) {
            MessageDigest md = MessageDigest.getInstance("SHA");
            md.update(signature.toByteArray());
            Log.d("KeyHash:", Base64.encodeToString(md.digest(), Base64.DEFAULT));
            }
    } catch (NameNotFoundException e) {

    } catch (NoSuchAlgorithmException e) {

    }
執行確認一下android hash key 是否正確。
5.Create a Facebook App
Create a new Facebook app, get its ID and associate your Android key hash with it
這邊重點注意,在Facebook developers中設定New APP的時候Sandbox(沙盒模式)記得要關閉,這樣這個APP才會變成"This app is live",這個沒轉換,也是會無法連結,在
if (session.isOpened()) 
這邊會ㄧ直為false,因為無法登入。

剩下步驟就沒有什麼問題,請參考官方網站說明

2013年5月27日 星期一

跳出UIPickerView來做選擇(有限資料)的做法



雖然覺得這樣的使用有點麻煩(太囉唆),但是還是記錄一下。



.h內的interface中加入UIPickerViewDelegate,UIActionSheetDelegate,UIPickerViewDataSource並且所使用的視圖元件要連接Outlets-Delegate要連接到File's Owners,否則無法抓到回傳的event。 
部分程式碼如下:

1.建立一個button並將Outlets-Delegate要連接到File's Owners
2.建立一個function內容如下,用button來觸發    
    //建立動作表
    UIActionSheet *SpeakSheet=[[UIActionSheet alloc]initWithTitle:@"\n\n\n\n\n\n\n\n\n\n\n\n\n" delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:@"set Sepaker", nil];
    //顯示動作表
    [SpeakSheet showInView:self.view];
    
    //建立挑選器視圖
    UIPickerView *SpeakPickerView = [[UIPickerView alloc]init];
    SpeakPickerView.tag=101;
    SpeakPickerView.delegate=self;
    SpeakPickerView.dataSource=self;
    SpeakPickerView.showsSelectionIndicator=YES;
    [SpeakSheet addSubview:SpeakPickerView];
3.如果有連接到File's Owner's就會被觸發,就可以在接到資訊
#pragma mark -
#pragma mark - SpeakPickerView methods
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
{
    if (pickerView.tag==101){
        return 1;
    }
    return 1;//N個轉軸
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
    if (pickerView.tag==101){
        return SpeakName.count;
    }
    return 1;//每個轉軸N個選項
}
- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component
{
    NSLog(@"component:%i  row:%i",component,row);
    if (pickerView.tag==101){
        return [SpeakName objectAtIndex:row];
    }
    return @"error";
}
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
    NSLog(@"clickedButtonAtIndex %i",buttonIndex);//回傳資料的地方
    //這裡沒寫完,要判斷要切換到誰,不過不重要,因為要換方法,所以就不再寫
    
}
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
    //當選定的時候從這邊抓取值,當按下click之後即可,讀取最正確地值作處理
    NSLog(@"didSelectRow");
}

2013年5月12日 星期日

[Android]製作簡易的自動更新機制

在Android上要怎麼製作簡易的自動更新機制呢?
1.按下一個button,送出去檢查apk版本,並比對網路上的版本,如果一致(不更新),不一致(更新),大概就是這樣,清楚明瞭。

大概幾個重點:
1.取得APK的版本:
   getPackageManager().getPackageInfo(getPackageName(),0).versionName;
2.下載:
   downLoadFile()   //參考此function
3.intent安裝
  Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(new File(path,fileName)),"application/vnd.android.package-archive");
startActivity(intent);

完整的code大致上如下所示:
//取得網站上的最新版本
    public void checkUpdateVersion() {
     try {
         //Log.e("tdy", "checkUpdate start");
            URL updateURL = new URL(Versionurl);              
            URLConnection conn = updateURL.openConnection();
            InputStream is = conn.getInputStream();
            BufferedInputStream bis = new BufferedInputStream(is);
            ByteArrayBuffer baf = new ByteArrayBuffer(50);
           
            int current = 0;
            while((current = bis.read()) != -1){
                 baf.append((byte)current);
            }

            /* Convert the Bytes read to a String. */
            final String s = new String(baf.toByteArray());        
           
            /* Get current Version Number */
            s_curVersion = getPackageManager().getPackageInfo(getPackageName(),0).versionName;
            s_newVersion = s;
            //Log.e("tdy",s_curVersion+" = "+s_newVersion);
            if (s_curVersion.equals(s_newVersion)){
             //Log.e("tdy","不需更新");  
             mHandler.post(showVersion);
            }else{
             //Log.e("tdy","需更新");
             mHandler.post(showUpdate);
            }
        } catch (Exception e) {
         //Log.e("tdy","網路發生錯誤");
         e.printStackTrace();         
        }
    }
    //更新資料
    private Runnable showUpdate = new Runnable(){
        public void run(){
         new AlertDialog.Builder(SpeakSettings.this)
         .setTitle("軟體更新")
         .setMessage("目前版本("+s_curVersion+")\n發現新版本("+s_newVersion+"),是否更新?")
         .setPositiveButton("更新", new DialogInterface.OnClickListener() {
                 public void onClick(DialogInterface dialog, int whichButton) {
                         /* User clicked OK so do some stuff */
                   pBar = new ProgressDialog(SpeakSettings.this);
      pBar.setTitle("正在下載");
      pBar.setMessage("請稍候...");
      pBar.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
      pBar.setIndeterminate(false);//設置進度條是否不明確
      pBar.setCancelable(true);
      pBar.setProgress(100);
      pBar.show();
      //開始下載
      Thread thread = new Thread(){
            public void run(){
              Message message = new Message();
         message.what = downLoadFile(fileurl, fileName, path);
         msghandler.sendMessage(message);
            }
      };
      thread.start();
      
                 }
         })
         .setNegativeButton("暫不更新", new DialogInterface.OnClickListener() {
                 public void onClick(DialogInterface dialog, int whichButton) {
                         /* User clicked Cancel */
                 }
         })
         .show();
        }
    };
    //show目前版本
    private Runnable showVersion = new Runnable(){
        public void run(){
         new AlertDialog.Builder(SpeakSettings.this)
         .setTitle("軟體更新")
         .setMessage("目前為最新版本("+s_curVersion+")")
         .setNegativeButton("好", new DialogInterface.OnClickListener() {
                 public void onClick(DialogInterface dialog, int whichButton) {
                         /* User clicked Cancel */
                 }
         })
         .show();
        }
    };
    //下載檔案
    public int downLoadFile(String httpUrl, String fileName, String path) {
     try {
      // 當存放文件的文件目錄不存在的時候創建該文件目錄
      File tmpFile = new File(path);
      if (!tmpFile.exists()) {
       tmpFile.mkdir();
      }
      //this is the file to be downloaded
   URL url = new URL(httpUrl);
   //create the new connection
   HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
   //set up some things on the connection
   urlConnection.setRequestMethod("GET");
   urlConnection.setDoOutput(true);
   //and connect!
   urlConnection.connect();

   File file = new File(path,fileName);
   //this will be used to write the downloaded data into the file we created
   FileOutputStream fileOutput = new FileOutputStream(file);
   //this will be used in reading the data from the internet
   InputStream inputStream = urlConnection.getInputStream();
   //this is the total size of the file
   int totalSize = urlConnection.getContentLength();
   //variable to store total downloaded bytes
   int downloadedSize = 0;
   //create a buffer...
   byte[] buffer = new byte[1024];
   int bufferLength = 0; //used to store a temporary size of the buffer
   //now, read through the input buffer and write the contents to the file
   while ( (bufferLength = inputStream.read(buffer)) > 0 ) {
    //add the data in the buffer to the file in the file output stream (the file on the sd card
    fileOutput.write(buffer, 0, bufferLength);
    //add up the size so we know how much is downloaded
    downloadedSize += bufferLength;
    //this is where you would do something to report the prgress, like this maybe
    //updateProgress(downloadedSize, totalSize);
    pBar.setProgress(downloadedSize*100/totalSize);    
   }
   //close the output stream when done
   fileOutput.close();
     }catch (Exception e) {
   // TODO: handle exception
      return 0;
  }
        return 1;
    }
    //顯示下載完畢或失敗的event
    public Handler msghandler = new Handler() {
  public void handleMessage(android.os.Message msg) {
   super.handleMessage(msg);
   if (msg.what == 1) {
    Toast.makeText(getApplicationContext(), "下載成功!!", Toast.LENGTH_LONG).show();
    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.setDataAndType(
      Uri.fromFile(new File(path,fileName)),
      "application/vnd.android.package-archive");
    startActivity(intent);
   } else {
    Toast.makeText(getApplicationContext(), "下載失敗!!", Toast.LENGTH_LONG).show();    
   }
   pBar.cancel();
  }
 };

2013年4月26日 星期五

[Android]設定delay幾秒後,執行某函式的方法。


//宣告一個Handler
Handler myHandler = new Handler();

//幾秒後(delaySec)呼叫runTimerStop這個Runnable,再由這個Runnable去呼叫你想要做的事情
myHandler.postDelayed(runTimerStop,delaySec*1000);


//移除
myHandler.removeCallbacks(runTimerStop);

//主體
private Runnable runTimerStop = new Runnable()
  {
      @Override
      public void run()
      {
         //想要做的事情
      }
   };

2013年4月16日 星期二

[Other]整理一些素材,提升網頁、App質感

剛開始整理,所以應該會很少,只是做個紀錄。

1.各式各樣的底圖,可以來這邊尋找。
subtle patterns: http://subtlepatterns.com/

2.idesign (各式各樣的背景圖)
http://idesigniphone.com

3.make app icon ( 製作符合iOS 或者Android的icon,給他一張1024X1024就可以產生所有的圖)
http://makeappicon.com

4.App Icon Template (提供psd的樣板可以修改,如果你會PS的話)
http://appicontemplate.com/ios8